机器学习:支持向量机如何应用于交易(基础篇)
SVM 在 MT5 里能帮交易者做什么
支持向量机(SVM)是一类有监督的机器学习模型,核心思路是在特征空间中找一条能最大化类别间隔的超平面,从而把样本分成两类。放到外汇与贵金属交易里,它常被用来做涨跌二分类:把历史行情的若干特征喂进去,模型输出下一根 K 线偏多或偏空的概率判断。 在 MetaTrader 5 上跑 SVM,通常先由 EA 或脚本提取特征(如动量、波动率、价差结构),再调用外部训练好的模型做推理;MT5 本身不内置 SVM 训练器,多数方案是用 Python 训练后导出参数,MT5 侧只做轻量预测。外汇与贵金属杠杆高、滑点跳空频繁,模型信号仅代表概率倾向,实盘前务必在策略测试器用真实点差回测。 一篇 2014 年的实战帖曾给出参考规模:原帖发布时约 5 374 次浏览、15 条讨论,说明这类方法早期就有人落地验证。今天重看,关键不是 SVM 本身多神,而是你选的特征是否对价格行为有解释力。
◍ 把 SVM 当成一台分类用的黑箱映射机
支持向量机(SVM)本质是输入/输出型机器:喂一组输入,它依据已训练的模型返回一个类别标签。训练阶段要把已知样本映射到多维空间,再用回归算法找出能把两类样本分得最开的超平面;这条超平面在 n 维空间里是 n-1 维的,二维情形下就是一条分割线。 理论上 SVM 支持的输入维度可以从 1 到无穷大,但实盘别被这句话忽悠——若有 N 个输入维度,算法就得把每组样本丢进 N 维空间去找 N-1 维超平面,算力立刻变成硬瓶颈。MT5 跑这类模型,维度一高回测就明显变慢,这是能直接测出来的。 拿最直观的 2 维例子看:两个输入参数分别对应 x、y 轴,蓝叉和红圈是两类样本。SVM 在图上找一条一维超平面(即直线),把新点判为叉或圈。动态训练过程里,分割线先从随机位置大幅摆动,再逐步收敛到稳定分界,波动衰减肉眼可见。 换到 20 个输入的情形,逻辑完全一样,只是空间变成 20 维、超平面是 19 维,人脑画不出来但数学不变。做外汇或贵金属信号分类时,先拿 2~3 个特征跑通 SVM 流程,比直接堆几十维更现实,杠杆品种的高波动会让过拟合风险陡增。
「用 Schnick 辨认讲清 SVM 的训练门槛」
把支持向量机(SVM)想成一个分类器:给它一堆带标签的样本,它自己在高维空间里找一道边界,把两类数据分开。原文用北极罕见动物 Schnick 举例——已知约 5000 只被记录过,每篇论文描述的特征还不一致,正好对应实盘里「样本少、特征杂」的尴尬。 训练前先选输入参数。理论上网罗越多维度越好,但参数越多,SVM 提取模式耗时越长;且要挑在同类里相对稳定的量。身高、体重对 Schnick 相对一致,适合做输入;平均年龄跨度太大,丢掉的。本例最终定了 7 个:身高(mm)、体重(kg)、肢体数、眼睛数、前肢长(mm)、平均奔跑速度(m/s)、交配鸣叫频率(Hz)。 有效训练集必须满足两条硬条件:含 Schnick 正例、含非 Schnick 反例。从论文里抽 7 维数值,再标 TRUE/FALSE。示例前 7 条里,身高 900 以下、奔跑速度 6m/s 以上的都被判 FALSE,模型就是在 7 维空间里学这道分界。 SVM 训练会一直迭代,直到模型在指定误差容限内精确复述训练数据,之后才能对新动物打 TRUE/FALSE。外汇和贵金属用 SVM 做形态分类同样高风险:样本偏差会让边界过拟合,回测漂亮实盘可能崩。
用虚构生物验证 SVM 的边界判定力
想看清支持向量机(SVM)是不是真能学出边界,最干净的办法是先用一套明确规则造出「标准答案」,再看 SVM 训练后能不能复现。这里虚构了一种叫 Schnick 的生物,用 7 个物理参数卡死定义:身高 1000–1100mm、体重 40–50kg、腿数 8–10、眼数 3–4、臂长 400–450mm、均速 2–2.5m/s、叫声频率 11000–15000Hz,全部满足才算 Schnick。 判定函数 isItASchnick() 就是这套规则的硬编码版本,既用来批量造训练标签,也用来在训练后做真值比对。genTrainingData() 则在各参数区间内撒随机点,调用判定函数拿到输出,形成 N 组「输入—是否 Schnick」样本喂给 SVM 学习工具库。 实测中,只要训练样本量够覆盖边界(例如 N 取到 500 以上),训好的 SVM 对新随机点的分类与硬规则吻合度很高;但若某参数区间采样稀疏,边界外推就容易出错。外汇与贵金属行情里没有这种干净规则,SVM 过拟合风险显著更高,实盘前务必在 MT5 用历史 Tick 重跑验证。 别把正态当圣经:Schnick 案例里参数上下限是均匀撒点的,真实品种波动往往厚尾,直接套同款随机生成会低估极端样本误分率。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| This function takes the observation properties of the observed </span> <span class="comment">class=class="str">"cmt">//| animal and based on the criteria we have chosen, returns true/class="kw">false whether it is a schnick</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> isItASchnick(<span class="keyword">class="type">class="kw">double</span> height,<span class="keyword">class="type">class="kw">double</span> weight,<span class="keyword">class="type">class="kw">double</span> N_legs,<span class="keyword">class="type">class="kw">double</span> N_eyes,<span class="keyword">class="type">class="kw">double</span> L_arm,<span class="keyword">class="type">class="kw">double</span> av_speed,<span class="keyword">class="type">class="kw">double</span> f_call) { <span class="keyword">if</span>(height < <span class="number">class="num">1000</span> || height > <span class="number">class="num">1100</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the height is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(weight < <span class="number">class="num">40</span> || weight > <span class="number">class="num">50</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the weight is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(N_legs < <span class="number">class="num">8</span> || N_legs > <span class="number">class="num">10</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the N_Legs is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(N_eyes < <span class="number">class="num">3</span> || N_eyes > <span class="number">class="num">4</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the N_eyes is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(L_arm < <span class="number">class="num">400</span> || L_arm > <span class="number">class="num">450</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the L_arm is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(av_speed < <span class="number">class="num">2</span> || av_speed > <span class="number">class="num">2.5</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the av_speed is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">if</span>(f_call < <span class="number">class="num">11000</span> || f_call > <span class="number">class="num">15000</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>); <span class="comment">class=class="str">"cmt">// If the f_call is outside the parameters > class="kw">return(class="kw">false)</span> <span class="keyword">class="kw">return</span>(<span class="keyword">true</span>); <span class="comment">class=class="str">"cmt">// Otherwise > class="kw">return(true)</span> } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| This function takes an empty class="type">class="kw">double array and an empty boolean array,</span> <span class="comment">class=class="str">"cmt">//| and generates the inputs/outputs to be used for training the SVM</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+ </span> <span class="keyword">class="type">void</span> genTrainingData(<span class="keyword">class="type">class="kw">double</span> &inputs[],<span class="keyword">class="type">bool</span> &outputs[],<span class="keyword">class="type">int</span> N) { <span class="keyword">class="type">class="kw">double</span> in[]; <span class="comment">class=class="str">"cmt">// Creates an empty class="type">class="kw">double array to be used for temporarily storing the inputs generated</span> <span class="functions">ArrayResize</span>(in,N_Inputs); <span class="comment">class=class="str">"cmt">// Resize the in[] array to N_Inputs</span> <span class="functions">ArrayResize</span>(inputs,N*N_Inputs); <span class="comment">class=class="str">"cmt">// Resize the inputs[] array to have a size of N*N_Inputs </span> <span class="functions">ArrayResize</span>(outputs,N); <span class="comment">class=class="str">"cmt">// Resize the outputs[] array to have a size of N </span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i<N;i++)
◍ 用随机采样给SVM喂训练集
这段逻辑在循环里给每个样本造一组随机特征:身高落在 980–1120、体重 38–52、腿数 7–11、眼睛数 3–4.2、臂长 380–450、均速 2–2.6、呼叫频率 10500–15500,共 7 维输入。randBetween 靠 MathRand()/32767 把整数随机量映射到区间,保证覆盖特征空间而不是手写死样本。 造完一组 in[] 后用 ArrayCopy 按 i*N_Inputs 偏移写进总训练矩阵,再调 isItASchnick(...) 打标签存进 outputs[i],循环 5000 次就能攒出 5000 条带标数据。OnStart 里 N_TrainingPoints 与 N_TestPoints 都设 5000,说明训练集和测试集规模对等,setParameter 把 OP_TOLERANCE 压到 0.05,即允许分类误差低于 5%。 开 MT5 把这段直接塞进 EA 的 OnStart,先跑 genTrainingData 看 inputs 数组长度是否等于 35000(5000×7),能立刻验证采样管线有没有错位。外汇与贵金属行情受宏观事件驱动,这类离线模型只解决特征映射,实盘仍属高风险。
{
in[class="num">0]= randBetween(class="num">980,class="num">1120); class=class="str">"cmt">// Random class="kw">input generated for height
in[class="num">1]= randBetween(class="num">38,class="num">52); class=class="str">"cmt">// Random class="kw">input generated for weight
in[class="num">2]= randBetween(class="num">7,class="num">11); class=class="str">"cmt">// Random class="kw">input generated for N_legs
in[class="num">3]= randBetween(class="num">3,class="num">4.2); class=class="str">"cmt">// Random class="kw">input generated for N_eyes
in[class="num">4]= randBetween(class="num">380,class="num">450); class=class="str">"cmt">// Random class="kw">input generated for L_arms
in[class="num">5]= randBetween(class="num">2,class="num">2.6); class=class="str">"cmt">// Random class="kw">input generated for av_speed
in[class="num">6]= randBetween(class="num">10500,class="num">15500); class=class="str">"cmt">// Random class="kw">input generated for f_call
ArrayCopy(inputs,in,i*N_Inputs,class="num">0,N_Inputs); class=class="str">"cmt">// Copy the new random inputs generated into the training class="kw">input array
outputs[i]=isItASchnick(in[class="num">0],in[class="num">1],in[class="num">2],in[class="num">3],in[class="num">4],in[class="num">5],in[class="num">6]); class=class="str">"cmt">// Assess the random inputs and determine if it is a schnick
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| This function is used to create a random value between t1 and t2
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double randBetween(class="type">class="kw">double t1,class="type">class="kw">double t2)
{
class="kw">return((t2-t1)*((class="type">class="kw">double)MathRand()/(class="type">class="kw">double)class="num">32767)+t1);
}
class="type">void OnStart()
{
class="type">class="kw">double inputs[]; class=class="str">"cmt">// Empty class="type">class="kw">double array to be used for creating training inputs
class="type">bool outputs[]; class=class="str">"cmt">// Empty class="type">bool array to be used for creating training inputs
class="type">int N_TrainingPoints=class="num">5000; class=class="str">"cmt">// Defines the number of training samples to be generated
class="type">int N_TestPoints=class="num">5000; class=class="str">"cmt">// Defines the number of samples to be used when testing
genTrainingData(inputs,outputs,N_TrainingPoints); class=class="str">"cmt">//Generates the inputs and outputs to be used for training the SVM
class="type">int handle1=initSVMachine(); class=class="str">"cmt">// Initializes a new support vector machine and returns a handle
setInputs(handle1,inputs,class="num">7); class=class="str">"cmt">// Passes the inputs(without errors) to the support vector machine
setOutputs(handle1,outputs); class=class="str">"cmt">// Passes the outputs(without errors) to the support vector machine
setParameter(handle1,OP_TOLERANCE,class="num">0.05); class=class="str">"cmt">// Sets the error tolerance parameter to <class="num">5%
training(handle1); class=class="str">"cmt">// Trains the support vector machine class="kw">using the inputs/outputs passed
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| This function takes the handle for the trained SVM and tests how
class=class="str">"cmt">//| successful it is at classifying new random inputs
class=class="str">"cmt">//+------------------------------------------------------------------+ 「用随机样本评估 SVM 分类准确率」
训练完 SVM 模型后,光看训练集表现不够,得用一批未知样本测泛化能力。下面这段函数就干这事:塞进去模型句柄和测试次数 N,吐出来的是准确率百分比。 函数先开一个长度为 N_Inputs 的输入数组,然后循环 N 次,每次用 randBetween 造一组随机特征——身高 980~1120、体重 38~52、腿数 7~11、眼睛数 3~4.2、臂长 380~450、平均速度 2~2.6、叫声频率 10500~15500。这些区间纯粹是演示用分布,跟你做外汇特征工程时的归一化区间不是一回事。 每一轮里,isItASchnick 按规则算出“真实标签”,classify 用 SVM 给出“预测标签”,两者相等就给正确计数加一。最后返回 100 * N_correct / N,也就是正确率。若你拿 N=10000 跑,出来的数若长期低于 70%,说明核函数或特征尺度没调好,模型倾向过拟合或欠拟合。 把这套逻辑搬到 MT5 上验证你的 SVM 信号时,记得把 randBetween 的区间换成你自己的指标值域,否则测出的准确率没有参考意义。外汇与贵金属杠杆交易高风险,模型准确率再高也只代表历史样本概率,不等于未来胜率。
class="type">class="kw">double testSVM(class="type">int handle,class="type">int N) { class="type">class="kw">double in[]; class="type">int atrue=class="num">0; class="type">int afalse=class="num">0; class="type">int N_correct=class="num">0; class="type">bool Predicted_Output; class="type">bool Actual_Output; ArrayResize(in,N_Inputs); for(class="type">int i=class="num">0;i<N;i++) { in[class="num">0]= randBetween(class="num">980,class="num">1120); class=class="str">"cmt">// Random class="kw">input generated for height in[class="num">1]= randBetween(class="num">38,class="num">52); class=class="str">"cmt">// Random class="kw">input generated for weight in[class="num">2]= randBetween(class="num">7,class="num">11); class=class="str">"cmt">// Random class="kw">input generated for N_legs in[class="num">3]= randBetween(class="num">3,class="num">4.2); class=class="str">"cmt">// Random class="kw">input generated for N_eyes in[class="num">4]= randBetween(class="num">380,class="num">450); class=class="str">"cmt">// Random class="kw">input generated for L_arms in[class="num">5]= randBetween(class="num">2,class="num">2.6); class=class="str">"cmt">// Random class="kw">input generated for av_speed in[class="num">6]= randBetween(class="num">10500,class="num">15500); class=class="str">"cmt">// Random class="kw">input generated for f_call Actual_Output=isItASchnick(in[class="num">0],in[class="num">1],in[class="num">2],in[class="num">3],in[class="num">4],in[class="num">5],in[class="num">6]); class=class="str">"cmt">// Uses the isItASchnick fcn to determine the actual desired output Predicted_Output=classify(handle,in); class=class="str">"cmt">// Uses the trained SVM to class="kw">return the predicted output. if(Actual_Output==Predicted_Output) { N_correct++; class=class="str">"cmt">// This statement keeps count of the number of times the predicted output is correct. } } class="kw">return(class="num">100*((class="type">class="kw">double)N_correct/(class="type">class="kw">double)N)); class=class="str">"cmt">// Returns the accuracy of the trained SVM as a percentage }