机器学习:支持向量机如何应用于交易(基础篇)
📘

机器学习:支持向量机如何应用于交易(基础篇)

第 1/3 篇

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 案例里参数上下限是均匀撒点的,真实品种波动往往厚尾,直接套同款随机生成会低估极端样本误分率。

MQL5 / C++
<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)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">if</span>(height&nbsp;&nbsp; &lt; <span class="number">class="num">1000</span>&nbsp;&nbsp;|| height&nbsp;&nbsp; &gt; <span class="number">class="num">1100</span>)&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the height is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(weight&nbsp;&nbsp; &lt; <span class="number">class="num">40</span>&nbsp;&nbsp;&nbsp;&nbsp;|| weight&nbsp;&nbsp; &gt; <span class="number">class="num">50</span>)&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the weight is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(N_legs&nbsp;&nbsp; &lt; <span class="number">class="num">8</span>&nbsp;&nbsp;&nbsp;&nbsp; || N_legs&nbsp;&nbsp; &gt; <span class="number">class="num">10</span>)&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the N_Legs is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(N_eyes&nbsp;&nbsp; &lt; <span class="number">class="num">3</span>&nbsp;&nbsp;&nbsp;&nbsp; || N_eyes&nbsp;&nbsp; &gt; <span class="number">class="num">4</span>)&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the N_eyes is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(L_arm&nbsp;&nbsp;&nbsp;&nbsp;&lt; <span class="number">class="num">400</span>&nbsp;&nbsp; || L_arm&nbsp;&nbsp;&nbsp;&nbsp;&gt; <span class="number">class="num">450</span>)&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the L_arm&nbsp;&nbsp;is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(av_speed &lt; <span class="number">class="num">2</span>&nbsp;&nbsp;&nbsp;&nbsp; || av_speed &gt; <span class="number">class="num">2.5</span>)&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the av_speed is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">if</span>(f_call&nbsp;&nbsp; &lt; <span class="number">class="num">11000</span> || f_call&nbsp;&nbsp; &gt; <span class="number">class="num">15000</span>) <span class="keyword">class="kw">return</span>(<span class="keyword">class="kw">false</span>);&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// If the f_call is outside the parameters &gt; class="kw">return(class="kw">false)</span>
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="keyword">true</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Otherwise &gt; class="kw">return(true)</span>
&nbsp;&nbsp;}
<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> &amp;inputs[],<span class="keyword">class="type">bool</span> &amp;outputs[],<span class="keyword">class="type">int</span> N)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> in[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(in,N_Inputs);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Resize the in[] array to N_Inputs</span>
&nbsp;&nbsp; <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>
&nbsp;&nbsp; <span class="functions">ArrayResize</span>(outputs,N);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Resize the outputs[] array to have a size of N </span>
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i&lt;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),能立刻验证采样管线有没有错位。外汇与贵金属行情受宏观事件驱动,这类离线模型只解决特征映射,实盘仍属高风险。

MQL5 / C++
  {
      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 的区间换成你自己的指标值域,否则测出的准确率没有参考意义。外汇与贵金属杠杆交易高风险,模型准确率再高也只代表历史样本概率,不等于未来胜率。

MQL5 / C++
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
  }

常见问题

SVM 主要把你整理好的行情特征映射成涨/跌或震荡的分类信号,适合做趋势过滤和入场前确认,但需要你先准备好特征数据。
把它当一台黑箱映射机:喂入历史K线的数值特征,它自动找出分隔不同行情类别的边界,你只需看输出类别而不必懂内部优化。
小布可替你加载特征、跑分类并标出当前品种所处的 SVM 边界区域,你直接看结论和概率即可,不必手动训练模型。
建议至少数百条涵盖多种行情的样本,并留出独立验证集;样本太少时边界会歪,分类准确率虚高。
用陌生随机样本测混淆矩阵,重点看错分集中在哪类行情;准确率超 95% 反而要警惕边界过窄只记了噪声。