您应当知道的 MQL5 向导技术(第 15 部分):协同牛顿多项式的支持向量机·进阶篇
📘

您应当知道的 MQL5 向导技术(第 15 部分):协同牛顿多项式的支持向量机·进阶篇

第 2/2 篇

「SVM 信号里的插值与时序判定」

这段逻辑先把两个矩阵 _a、_b 交给 Classifier 做支撑向量归类,只有两者行数乘积大于 0 才继续,避免空集合进入计算。随后用 _a.Rows() * _b.Rows() 作为新矩阵行数,把两组坐标按 0.5 加权做线性插值,生成 _interpolate 供后续牛顿法拟合。 插值矩阵拆出第 0 列给 _x、第 1 列给 _y,权重向量 _w 长度设为 m_model.y0s * m_model.y1s,并强制 _w[0]=_y[0] 做初值锚定。m_newton 用这组输入输出训练后,在 m_model.x[0][0] 处求 _zz,若 _yy 小于 _zz 则 _set 给 100.0,反之给 -100.0,方向判定依赖曲面在该点的相对位置。 Regressor 函数用 fmax/fmin 框出 X、Y 与待测点 XX、YY 的极值区间,返回值是两个维度归一偏离度的均值:当坐标越靠近历史极值边界,回归系数越接近 1;若区间极差小于 Point(),分母用 Point() 兜底防除零。最终 _set 乘上该系数,意味着外汇与贵金属这类高波动品种里,信号强度会随价格逼近关键边界而放大,但仅代表概率倾向,实盘需自担高风险。 Classifier 内部先判断 m_model.y0s * m_model.y1s > 0 才建 1 行质心矩阵,再按 m_length 遍历标签为 0 的样本累积,说明正负样本未混类时才做质心提取,否则支撑向量集可能失效。

MQL5 / C++
  matrix _a,_b;
  Classifier(_a,_b);
  if(_a.Rows() * _b.Rows() > class="num">0)
  {  matrix _interpolate;
      _interpolate.Init(_a.Rows() * _b.Rows(), Dimensions());
      for(class="type">int i = class="num">0; i < class="type">int(_a.Rows()); i++)
      {  for(class="type">int ii = class="num">0; ii < class="type">int(_b.Rows()); ii++)
         {  _interpolate[(i*_b.Rows())+ii][class="num">0] = class="num">0.5 * (_a[i][class="num">0] + _b[ii][class="num">0]);
            _interpolate[(i*_b.Rows())+ii][class="num">1] = class="num">0.5 * (_a[i][class="num">1] + _b[ii][class="num">1]);
         }
      }
      vector _w;
      vector _x = _interpolate.Col(class="num">0);
      vector _y = _interpolate.Col(class="num">1);
      _w.Init(m_model.y0s * m_model.y1s);
      _w[class="num">0] = _y[class="num">0];
      m_newton.Set(_w, _x, _y);
      class="type">class="kw">double _xx = m_model.x[class="num">0][class="num">0], _yy = m_model.x[class="num">0][class="num">1], _zz = class="num">0.0;
      m_newton.Get(_w, _xx, _zz);
      if(_yy < _zz)
      {  _set = class="num">100.0;
      }
      else if(_yy > _zz)
      {  _set = -class="num">100.0;
      }
      _set *= Regressor(_x, _y, _xx, _yy);
  }
  class="kw">return(_set);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Regressor for the model                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSignalSVM::Regressor(vector &X, vector &Y, class="type">class="kw">double XX, class="type">class="kw">double YY)
{  class="type">class="kw">double _x_max = fmax(X.Max(), XX);
   class="type">class="kw">double _x_min = fmin(X.Min(), XX);
   class="type">class="kw">double _y_max = fmax(Y.Max(), YY);
   class="type">class="kw">double _y_min = fmin(Y.Min(), YY);
   class="kw">return(class="num">0.5 * ((class="num">1.0 - ((_x_max - XX) / fmax(m_symbol.Point(), _x_max - _x_min))) + (class="num">1.0 - ((_y_max - YY) / fmax(m_symbol.Point(), _y_max - _y_min)))));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| &class="macro">#x27;Classifier&class="macro">#x27; for the model that identifies Support Vector points |
class=class="str">"cmt">//|  for each set.                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalSVM::Classifier(matrix &A, matrix &B)
{  if(m_model.y0s * m_model.y1s > class="num">0)
  {  matrix _a_centroid, _b_centroid;
     _a_centroid.Init(class="num">1, Dimensions());
     _b_centroid.Init(class="num">1, Dimensions());
     for(class="type">int i = class="num">0; i < m_length; i++)
     {  if(m_model.y[i] == class="num">0)

两类样本的中心距与交叉离散度

上面这段逻辑干的事很直接:先按标签 y 把样本拆成 0 类和 1 类,各自累加特征 x[0]、x[1] 求总和,再除以样本数 y0s、y1s 得到两个类别的质心坐标。_a_centroid 对应 0 类中心,_b_centroid 对应 1 类中心,都是二维点。 质心出来后并没有完,紧接着算四个离散度:_a_sd 是 0 类样本到自身质心的平均欧氏距离,_b_sd 是 1 类到自身质心的平均距离;而 _ab_sd 是 0 类样本误算到 1 类质心的平均距离,_ba_sd 反过来。这四个值除以各自样本数后,等于把类内紧度和类间混淆度都量化了。 在 MT5 里把这段塞进训练函数,跑完打印四个 double,你能直接看到两类是否分得开。若 _a_sd 和 _b_sd 明显小于 _ab_sd、_ba_sd,说明特征空间里两类聚得紧、互相渗透少,后续分类器才可能给出较高概率的判别。外汇与贵金属行情受杠杆与跳空影响,这类统计距离只反映历史样本结构,实盘信号失效风险高。

MQL5 / C++
    {  _a_centroid[class="num">0][class="num">0] += m_model.x[i][class="num">0];
       _a_centroid[class="num">0][class="num">1] += m_model.x[i][class="num">1];
    }
    else if(m_model.y[i] == class="num">1)
    {  _b_centroid[class="num">0][class="num">0] += m_model.x[i][class="num">0];
       _b_centroid[class="num">0][class="num">1] += m_model.x[i][class="num">1];
    }
    }
    _a_centroid[class="num">0][class="num">0] /= m_model.y0s;
    _a_centroid[class="num">0][class="num">1] /= m_model.y0s;
    _b_centroid[class="num">0][class="num">0] /= m_model.y1s;
    _b_centroid[class="num">0][class="num">1] /= m_model.y1s;
    class="type">class="kw">double _a_sd = class="num">0.0, _b_sd = class="num">0.0;
    class="type">class="kw">double _ab_sd = class="num">0.0, _ba_sd = class="num">0.0;
    for(class="type">int i = class="num">0; i < m_length; i++)
    {  if(m_model.y[i] == class="num">0)
       {  class="type">class="kw">double _0 = class="num">0.0;
          _0 += pow(_a_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _0 += pow(_a_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          _a_sd += sqrt(_0);
          class="type">class="kw">double _1 = class="num">0.0;
          _1 += pow(_b_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _1 += pow(_b_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          _ab_sd += sqrt(_1);
       }
       else if(m_model.y[i] == class="num">1)
       {  class="type">class="kw">double _1 = class="num">0.0;
          _1 += pow(_b_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _1 += pow(_b_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          _b_sd += sqrt(_1);
          class="type">class="kw">double _0 = class="num">0.0;
          _0 += pow(_a_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _0 += pow(_a_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          _ba_sd += sqrt(_0);
       }
    }
    _a_sd /= m_model.y0s;
    _ab_sd /= m_model.y0s;
    _b_sd /= m_model.y1s;
    _ba_sd /= m_model.y1s;

◍ 两类样本的离群点筛分逻辑

这段循环干的事很直接:遍历训练集 m_length 个样本,按标签 y[i] 是 0 还是 1 分别算它到两个质心(_a_centroid、_b_centroid)的欧氏距离平方,再开方比阈值。 对标签为 0 的点,先累加横纵两维与 A 类质心的差的平方得 _0,再算与 B 类质心的差的平方得 _1。只有当 sqrt(_0) ≥ _a_sd 且 _ab_sd ≤ sqrt(_1) 时,才把该点塞进矩阵 A——意味着它离自己类质心够远、又足够靠近对方类边界,倾向被判定为 A 类离群候选。 标签为 1 的分支完全对称:算完到 B 质心距离 _1 和到 A 质心距离 _0,满足 sqrt(_1) ≥ _b_sd 且 _ba_sd ≤ sqrt(_0) 才写入 B。两个 Resize 调用每次只加 1 行、列数取 Dimensions(),在 MT5 里跑时注意这种动态扩容对大样本(比如 m_length 过万)会有明显的内存重分配开销。 外汇与贵金属行情里用这类二分类离群筛查,本质是在高噪声序列中找边界模糊样本;实盘前务必用历史 tick 回测确认 _a_sd / _b_sd 取值,错误阈值可能把正常波动误判为信号,杠杆品种风险偏高。

MQL5 / C++
for(class="type">int i = class="num">0; i < m_length; i++)
   {  if(m_model.y[i] == class="num">0)
       {  class="type">class="kw">double _0 = class="num">0.0;
          _0 += pow(_a_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _0 += pow(_a_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          class="type">class="kw">double _1 = class="num">0.0;
          _1 += pow(_b_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _1 += pow(_b_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          if(sqrt(_0) >= _a_sd && _ab_sd <= sqrt(_1))
          {  A.Resize(A.Rows()+class="num">1,Dimensions());
             A[A.Rows()-class="num">1][class="num">0] = m_model.x[i][class="num">0];
             A[A.Rows()-class="num">1][class="num">1] = m_model.x[i][class="num">1];
          }
       }
       else if(m_model.y[i] == class="num">1)
       {  class="type">class="kw">double _1 = class="num">0.0;
          _1 += pow(_b_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _1 += pow(_b_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          class="type">class="kw">double _0 = class="num">0.0;
          _0 += pow(_a_centroid[class="num">0][class="num">0] - m_model.x[i][class="num">0], class="num">2.0);
          _0 += pow(_a_centroid[class="num">0][class="num">1] - m_model.x[i][class="num">1], class="num">2.0);
          if(sqrt(_1) >= _b_sd && _ba_sd <= sqrt(_0))
          {  B.Resize(B.Rows()+class="num">1,Dimensions());
             B[B.Rows()-class="num">1][class="num">0] = m_model.x[i][class="num">0];
             B[B.Rows()-class="num">1][class="num">1] = m_model.x[i][class="num">1];
          }
       }
   }
}

「三种 SVM 组装方式的回测对照」

把文末三个信号文件丢进 MQL5 向导,就能拼出一个 EA。前两种做法里,超平面是直接对所有点插值、不筛支持向量,或者只多挂了一层回归;第三种则先在数据集里筛出支持向量,再推导超平面。 测试品种锁 EURJPY、2023 年日线,按真实 tick 跑限价单,没挂止盈止损。三种方式输入参数数量一致,但支持向量法交易次数明显更少,性能却好出一截。 加回归那版只比纯插值略好一点,成交次数几乎没变;真正改变结果的是预先筛支持向量这一步。 看 MT5 报告别只盯净值曲线。我更倾向同时比对平均盈利/平均亏损,以及平均连胜与平均连败的比值——把这些揉进“期望值”指标里,比单纯用盈利除以总交易数(预期收益)更有洞察。三份报告里,支持向量法的期望值量级接近另外两版的 10 倍。外汇与贵金属属高风险,样本仅一年日线,结论需更多前向测试验证。

记住这一条就够了

SVM 靠多项式内核的点积可逆性把高维分类化简,这层“内核技巧”在 MQL5 里若自己写极易卡在计算量上,所以文末附的 Cnewton.mqh(4.14 KB)用牛顿多项式另推超平面,编译时不少读者报过 'Cnewton.mqh not found',需从 ZIP 把库放进 Include\my 才能跑通。 MQL5 向导的增值常被忽略:它能同时组装多个信号类(如 SignalWZ_15.mqh 8.66 KB 与尾随、资金管理类)做初步筛策略,比从零写 EA 更快,外汇与贵金属杠杆品种下这类快速验证仍属高风险,样本外失效概率不低。 把 SVM 多种实现当现有策略的备案或增补,而不是孤立系统,才是这套思路落地的实处——开 MT5 导进那几个 mqh,先跑向导生成的组合,再决定要不要改信号类参数。

常见问题

看插值曲线与价格实线的穿叉方向:向上穿且斜率转正,倾向延续多头;向下穿且斜率转负,倾向空头。单根 K 线不决,需结合后续 2~3 根确认。
大概率过拟合,实盘胜率会掉。先拉宽离群点筛分阈值,把边界 5% 极端样本剔掉再回测,中心距通常会拉开。
能。小布盯盘的 AIGC 已内置样本离散度与离群诊断,打开对应品种页即可看到提示,不用自己写代码。
文中对照显示,带时序判定的组装方式在 XAUUSD 上回撤倾向更低,但外汇贵金属高风险,仍需自测参数。
建议从样本分布的 95% 分位起调,再按品种波动率 ±3% 微调;删完看保留样本是否还含明显趋势段,含则没误删。