在算法交易中 KOHONEN 神经网络的实际应用  第二部分优化和预测(基础篇)
📘

在算法交易中 KOHONEN 神经网络的实际应用 第二部分优化和预测(基础篇)

第 1/3 篇

「从修正类到实盘:Kohonen 图的两类用法」

在系列前一篇里,我们已把公开的 Kohonen 神经网络类做了修正,补齐了训练所需的核心算法。类能跑通只是第一步,真正对交易者有用的是把它接进 MT5 的实际问题里。 这一节先定下落地方向:用 Kohonen 自组织映射图(SOM)做两件事——一是在 EA 参数空间里挑出较优组合,二是把价格序列投到网络上做形态预测。两者都不依赖未来函数,输入只来自已发生的行情与回测结果。 需要提醒,外汇与贵金属杠杆高、滑点随机,网络给出的参数倾向或形态概率只作筛选参考,不能直接当成下单依据。开 MT5 把前篇修正过的神经类挂上,先准备一组 EA 历史回测参数表,后面两节会直接套用到这张图上。

◍ 用聚类高原替代极值选参

MT5 测试器跑完优化,常吐出几千行报告。按利润或夏普排序,顶部往往是一堆数值相同、参数迥异的设置——遗传算法本就‘陷’在盈利通道里,把亏损版本早筛掉了,导致参数平面分布畸变,Kohonen 图容易漏掉好参数旁边的‘危险谷’。EURUSD D1 上 2018 年 1–6 月遗传优化 WizardTest,剔掉交易量低于 5 且夏普异常的条目、只留利润≥10000 后,csv 还剩约 2000 行,大量行盈利指标一致而参数不同。 选参不该追目标函数最大值,而应找函数值区间内最长持续的‘高原’:高原水平≈平均盈利能力,长度≈稳健性。用 K-Means 或 Kohonen 聚类搜高原时,簇数无通用标准——太多则过拟合、太小则欠拟合,上限受图边长约束(前述公式 7 定边长 15,则簇数不超 15),建议多跑几次实验。 为让网络只按 EA 参数或只按经济指标聚,给 CSOMNode 加 dimensionMax 与 dimensionBitMask:前者截断末位组件,后者用位掩码随机屏蔽。训练前设好掩码,距离计算即排除指定维;默认 ApplyFeatureMaskAfterTraining=false,若置 true 则在 Train 后调 SetFeatureMask,支持‘无掩码训练、掩码找簇’。 实测:15×15 网跑 100 代,全特征聚出的簇 0 正向测试利润不比随机首行好;改用前 6 维(仅 EA 参数)掩码重聚,U 矩阵与簇结构明显变样。从约 2000 行里专家肉眼挑右上角稳定区,正向利润跑赢均值 1007+标准差 1444;自动簇 0 无掩码时明显逊于专家。外汇/贵金属实盘参数敏感,左一步右一步进损失谷的概率不低,掩码隔离经济指标是避坑硬操作。

用RGB映射替代神经元邻近度来挑参数

Kohonen 图的聚类质量直接决定 EA 最优参数搜索的准确度,但比对神经元空间邻近度这套做法有点重。换一条路:把特征权重直接画进 RGB 色彩空间,挑最浅的神经元,相当于用肉眼做三维特征排序。 RGB 模型正好塞得下 3 个特征平面。特征多于 3 个时就退化为灰度渐变,但最浅点依旧可定位。挑选特征时尽量取相互独立的量,前文已提过这一点。 实现上不必重写整套 SOM 逻辑,直接从 CSOMDisplay 派生 CSOMDisplayRGB,只覆写几个虚方法,让显示器输出 RGB 映射而非最后一个 DIM_OUTPUT 平面。SOM-Explorer-RGB 改造版只增加了一个输入参数开启 RGB 模式,图对象换成派生类,显示分支单独走。 复用已有的 FeatureDirection 参数就能指定进 RGB 空间的特征。Wizard2018plus 示例里写 ++,-,,,:利润、PF 进图(对应 +),回撤反向进图(对应 -),其余逗号跳过。附带文件 WizardTest-rgb.set 里网络文件 Wizard2018plus.som 沿用上一阶段产物。 左侧 RGB 空间里利润=红、PF=绿、回撤=蓝,最亮神经元标了「RGB」,日志打出其坐标与特征值作为候选。因利润与 PF 强相关,换掩码跳过利润、加交易数(部分券商按交易量返佣),右侧颜色映射变成 PF=红、回撤=绿、交易数=蓝。 只留 PF 和回撤就掉回灰度图;单留利润则黑白模式与第一平面完全对应,可用来校验渲染没出错。 把正向测试接进来分析也顺手:MetaTrader 测试器优化后带 forward 测试,把 WizardTest 的优化(20180101-20180701)与 forward(20180701-20181201)合并,只留过去/未来利润两列生成 Wizard2018-with-forward.csv。全特征教学后,未来利润区域明显比过去利润区小,零集群(红)含该位置;按 ++ 掩码取最浅点也落此 cluster,但中心与浅点都不含超级盈利设置——比早前结果差约两倍。 改用 FeatureMask=1100000 只留前两特征重连网络,左下角高值区在末图被高亮,取该神经元设置做 tester 报告,同样弱于先前。实盘实验没捞出最佳参数,但把 forward 结果喂进神经网分析的方向是对的,本例测试 EA 可能本就不适合稳定交易,细节可在评论区拆。

MQL5 / C++
  class="kw">static class="type">int dimensionMax;
  class="kw">static class="type">class="kw">ulong dimensionBitMask;
class="kw">static class="type">void CSOMNode::SetFeatureMask(class="kw">const class="type">int dim = class="num">0, class="kw">const class="type">class="kw">ulong bitmask = class="num">0)
{
  dimensionMax = dim;
  dimensionBitMask = bitmask;
}
class="type">class="kw">double CSOMNode::CalculateDistance(class="kw">const class="type">class="kw">double &vector[]) class="kw">const
{
  class="type">class="kw">double distSqr = class="num">0;
  if(dimensionMax <= class="num">0 || dimensionMax > m_dimension) dimensionMax = m_dimension;
  for(class="type">int i = class="num">0; i < dimensionMax; i++)
  {

「用位掩码挑特征维度」

自组织映射(SOM)里不是所有输入维度都得参与距离计算。代码用 dimensionBitMask 做开关:某位为 1 才把该维的 (vector[i]-m_weights[i])² 累加进 distSqr,全 0 则退化为全维度欧氏距离。 SetFeatureMask 把外部传进来的 ulong 位掩码存进 m_featureMask,并遍历 0..m_dimension-1 统计置位个数到 m_featureMaskSize,同时 Print 出对应 m_titles[i] 方便在 MT5 日志里核对启用了哪些特征。 从字符串解析掩码时,FeatureMask 参数最长截 64 字符(MathMin(StringLen,64)),逐字符遇 '1' 就把 1 左移 i 位 OR 进 mask,再交给 KohonenMap.SetFeatureMask(0,mask)。 LoadSettings 读文本配置:以 ';' 开头的行跳过,按 '=' 拆名值,再按 '|' 拆首个浮点写进 v[index];只装载 KohonenMap.FindFeature 能查到的特征,最后打印实际命中数 count。外汇与贵金属行情高波动,这类特征筛选仅降低维度噪声,不预示方向。

MQL5 / C++
if(dimensionBitMask == class="num">0 || ((dimensionBitMask & (class="num">1 << i)) != class="num">0))
{
  distSqr += (vector[i] - m_weights[i]) * (vector[i] - m_weights[i]);
}
}
class="kw">return distSqr;
}
class="type">void CSOM::SetFeatureMask(class="kw">const class="type">int dim, class="kw">const class="type">class="kw">ulong bitmask)
{
  m_featureMask = class="num">0;
  m_featureMaskSize = class="num">0;
  if(bitmask != class="num">0)
  {
    m_featureMask = bitmask;
    Print("启用了特性掩码:");
    for(class="type">int i = class="num">0; i < m_dimension; i++)
    {
      if((bitmask & (class="num">1 << i)) != class="num">0)
      {
        m_featureMaskSize++;
        Print(m_titles[i]);
      }
    }
  }
  CSOMNode::SetFeatureMask(dim == class="num">0 ? m_dimension : dim, bitmask);
}
  class="type">class="kw">ulong mask = class="num">0;
  if(FeatureMask != "")
  {
    class="type">int n = MathMin(StringLen(FeatureMask), class="num">64);
    for(class="type">int i = class="num">0; i < n; i++)
    {
      mask |= (StringGetCharacter(FeatureMask, i) == &class="macro">#x27;class="num">1&class="macro">#x27; ? class="num">1 : class="num">0) << i;
    }
  }
  KohonenMap.SetFeatureMask(class="num">0, mask);
class="type">bool LoadSettings(class="kw">const class="type">class="kw">string filename, class="type">class="kw">double &v[])
{
  class="type">int h = FileOpen(filename, FILE_READ | FILE_TXT);
  if(h == INVALID_HANDLE)
  {
    Print("FileOpen 错误 ", filename, " : ",GetLastError());
    class="kw">return class="kw">false;
  }
  
  class="type">int n = KohonenMap.GetFeatureCount();
  ArrayResize(v, n);
  ArrayInitialize(v, EMPTY_VALUE);
  class="type">int count = class="num">0;
  class="kw">while(!FileIsEnding(h))
  {
    class="type">class="kw">string line = FileReadString(h);
    if(StringFind(line, ";") == class="num">0) class="kw">continue;
    class="type">class="kw">string name2value[];
    if(StringSplit(line, &class="macro">#x27;=&class="macro">#x27;, name2value) != class="num">2) class="kw">continue;
    class="type">int index = KohonenMap.FindFeature(name2value[class="num">0]);
    if(index != -class="num">1)
    {
      class="type">class="kw">string values[];
      if(StringSplit(name2value[class="num">1], &class="macro">#x27;|&class="macro">#x27;, values) > class="num">0)
      {
        v[index] = StringToDouble(values[class="num">0]);
        count++;
      }
    }
  }
  
  Print("Settings loaded: ", filename, "; features found: ", count);
  
  class="type">class="kw">ulong mask = class="num">0;
  for(class="type">int i = class="num">0; i < n; i++)
  {
    if(v[i] != EMPTY_VALUE)
    {

◍ 把.set配置喂给自组织映射做模式匹配

这段逻辑处理的是以 .set 结尾的文件:先判断文件名末四位是否为「.set」,是则把文件里的参数数组读进 double 型 v[],再交给 LoadSettings 解析。若读取成功(count>0),就通过 KohonenMap.AddPattern(v,"SETTINGS") 把这组 EA 参数当作一个模式塞进自组织映射网络。 匹配阶段调用 GetBestMatchingFeatures(v,y) 找出最相近的节点,打印其坐标、命中次数 node.GetHitsCount()、均方误差 node.GetMSE() 和聚类编号。随后 CalculateOutput(v,true) 强制重算输出,hasOneTestPattern 置真,意味着后续不再随机生成样本而是拿这组真实配置跑网络。 特征掩码那几行也别忽略:循环里若第 i 维有效就 mask |= (1<<i) 置位,否则 v[i]=0;mask 非 0 时 KohonenMap.SetFeatureMask(0,mask) 只让有效维度参与距离计算,避免空特征污染节点权重。 下面这段 input 是 EA 外壳参数,MagicNumber 写死 17897,开仓/平仓信号阈值都是 10(取值区间 0~100),止损止盈各 50 点。外汇与贵金属杠杆高、滑点跳空频繁,这类默认 50 点止损在黄金分钟线可能一两根影线就被扫,实盘前务必在 MT5 策略测试器里按品种波动重调。

MQL5 / C++
    mask |= (class="num">1 << i);
  }
  else
  {
    v[i] = class="num">0;
  }
}
if(mask != class="num">0)
{
  KohonenMap.SetFeatureMask(class="num">0, mask);
}
FileClose(h);
class="kw">return count > class="num">0;
}
      class=class="str">"cmt">// 以特定模式方式处理 .set 文件
      if(StringFind(DataFileName, ".set") == StringLen(DataFileName) - class="num">4)
      {
        class="type">class="kw">double v[];
        if(LoadSettings(DataFileName, v))
        {
          KohonenMap.AddPattern(v, "SETTINGS");
          ArrayPrint(v);
          class="type">class="kw">double y[];
          CSOMNode *node = KohonenMap.GetBestMatchingFeatures(v, y);
          Print("Matched Node Output(", node.GetX(), ",", node.GetY(),
                "); Hits:", node.GetHitsCount(), "; Error:", node.GetMSE(),
                "; Cluster N", node.GetCluster(), ":");
          ArrayPrint(y);
          KohonenMap.CalculateOutput(v, true);
          hasOneTestPattern = true;
        }
      }
class="kw">input class="type">class="kw">string        Expert_Title        ="WizardTest"; class=class="str">"cmt">// 文档名称
class="type">class="kw">ulong               Expert_MagicNumber  =class="num">17897;        class=class="str">"cmt">//
class="type">bool                Expert_EveryTick    =class="kw">false;        class=class="str">"cmt">//
class=class="str">"cmt">//--- 主信号的输入参数
class="kw">input class="type">int           Signal_ThresholdOpen   =class="num">10;        class=class="str">"cmt">// open [class="num">0...class="num">100] 的信号阈值
class="kw">input class="type">int           Signal_ThresholdClose  =class="num">10;        class=class="str">"cmt">//  close [class="num">0...class="num">100] 的信号阈值
class="kw">input class="type">class="kw">double        Signal_PriceLevel      =class="num">0.0;       class=class="str">"cmt">// 执行交易的价格水平
class="kw">input class="type">class="kw">double        Signal_StopLevel       =class="num">50.0;      class=class="str">"cmt">// 止损水平 (点数)
class="kw">input class="type">class="kw">double        Signal_TakeLevel       =class="num">50.0;      class=class="str">"cmt">// 获利水平 (点数)

常见问题

看同一颜色区块内样本点密度是否连续成片、误差变化平缓;若只是零星几个点请直接忽略,实盘前用历史数据回测该区稳定性。
更直观。把三维指标映射成红绿蓝通道生成热力图,颜色过渡均匀的区域即低冲突参数区,比手动比邻居距离省力且不易漏看。
可以。把品种和特征维度告诉小布,它会生成聚类图并标出高密度参数高原,你只需确认是否接入实盘。
高频周期下的动量+波动率组合最易过拟合;建议掩码保留 3 个以内低频特征,并用样本外数据验证。
说明市场状态切换,旧簇失效;重跑近 3 个月数据更新映射,偏离超 30% 暂停该配置实盘。