在 MetaTrader 5 中使用自组织特征映射(Kohonen 映射)(基础篇)
📘

在 MetaTrader 5 中使用自组织特征映射(Kohonen 映射)(基础篇)

第 1/3 篇

「在 MT5 里跑通 Kohonen 自组织映射」

自组织特征映射(SOM,也称 Kohonen 网络)是一种无监督神经网络,能把高维输入压缩到二维网格,使相似样本在网格上相邻。在 MT5 中可用 MQL5 直接实现,不依赖外部 Python 或 R。 这套思路适合把多周期波动率、动量、价差结构等特征做聚类,让交易者肉眼看出当前行情落在历史样本的哪个区域。外汇与贵金属杠杆高、滑点突变频繁,聚类结果只代表历史形态相似度,不预示方向,实盘须自担高风险。 下面这段是最小可用的网格训练骨架:先定义神经元权重数组,再用随机样本迭代更新。你可以直接贴进 MT5 的脚本里跑,观察 winner 索引变化。

MQL5 / C++
class="type">class="kw">double weights[class="num">10][class="num">5]; class=class="str">"cmt">// 10个神经元, 每个5维输入
class="type">int winner;
for(class="type">int n=class="num">0;n<class="num">1000;n++){
  class="type">class="kw">double sample[class="num">5];
  for(class="type">int i=class="num">0;i<class="num">5;i++) sample[i]=MathRand()/class="num">32767.0;
  class="type">class="kw">double best=class="num">1e9;
  for(class="type">int k=class="num">0;k<class="num">10;k++){
    class="type">class="kw">double d=class="num">0;
    for(class="type">int i=class="num">0;i<class="num">5;i++) d+=(sample[i]-weights[k][i])*(sample[i]-weights[k][i]);
    if(d<best){best=d;winner=k;}
  }
  for(class="type">int i=class="num">0;i<class="num">5;i++) weights[winner][i]+=class="num">0.1*(sample[i]-weights[winner][i]);
}

SOM 是什么以及它在 MT5 里怎么落地

自组织特征映射(SOM)是一类无监督训练的人工神经网络,核心是把高维输入空间压成二维离散网格,方便做分类和可视化。这东西是芬兰教授 Teuvo Kohonen 先提出来的,所以圈内也直接叫 Kohonen 映射,本质上跟多维标度思路接近,但带上了神经网络的权重自适应。 在 MT5 里跑这套,可视化不能靠常规图表控件,得自己画。我们借助 cIntBMP 这个能生成 BMP 图像的库,把映射结果写成位图文件,再在客户端里调出来看。原始算法参考了 ai-junkie 上面的实现版本,后续会直接搬进 MQL5 代码里改。 本篇先不堆数学,重点是把 Kohonen 映射的两个简单用法在 MT5 中跑通:一个是把行情特征降维后看聚类,一个是据此做可视化的状态区分。外汇和贵金属波动高、跳空频繁,降维结果只反映历史样本结构,对后续走势只具概率性参考。

◍ 无监督降维的 Kohonen 映射怎么干活

自组织特征映射(SOM)由 Teuvo Kohonen 在 1982 年提出,它不要求输入与输出有一一对应,靠无监督学习自己摸数据分布。本质上,它把高维输入的非线性统计关系,压到一张两维网格上,变成点与点之间的几何邻近关系,分类和可视化一下就直观了。 一个最朴素的例子是 4x4 共 16 个节点的 Kohonen 网格,每个节点挂一个三维权重向量,并在栅格里有 (x,y) 坐标。训练时不需要你给目标值,而是让权重跟输入向量对得上的那片栅格区域被反复优化,最后每个稳定区域都相当于一个特征分类器。 具体步骤是:先随机初始化全部节点权重;再从训练集随机抽一个向量,扫一遍所有节点找出权重最靠近它的那个——叫最佳匹配单元(BMU);接着算 BMU 的邻域半径,初始等于整张栅格半径,之后逐轮收缩。半径内的节点按离 BMU 远近调权重,越近调得越狠。这套动作重复 N 次迭代就出映射。 开 MT5 自己写个 4x4 网格跑两组三维随机点,观察迭代 500 次前后节点权重的聚集情况,比看文字描述来得快。外汇与贵金属价格序列做这类降维前务必做标准化,这类品种波动聚集、跳空频繁,直接喂原始价容易让映射失真。

「用 SOM 把颜色、食物和鸢尾花压成一张图」

自组织映射(SOM)最直观的练手素材是颜色聚类。取 8 个 RGB 向量如红 (255,0,0)、深蓝 (0,0,139)、紫 (128,0,128),用 CSOMNode 与 CSOM 两个类在 MT5 里跑网格训练,映射在经过 2400 步后稳定成型。把节点扩到 300 个、画布设 400x400,就能看到聚类被清晰本地化;改六边形单元格并设 ShowBorders=false,可去掉边框只留色块。 Web 颜色常量直接进训练集更省事。从 CSOM 派生 CSOMWeb,无需手填分量即可把一组接近色扔进网络。启动 som-ex2-hex.mq5 后能看到聚类,但像 xxxBlue 这类分量极近的色会被撕到不同区域——这是训练集结构导致的,不是 bug。 食物表用蛋白/碳水/脂肪三维向量分组,25 项里牛排(20.9,0,7.9)、黄油(1,0,81)、糖(0,95.1,0) 因数值跨度大,在 RGB 映射里分别偏红、蓝、绿。代码里用 m_max_values[] 和 m_min_values[] 做定标,重载 Train()/Render() 即可。 四维的 Fisher 鸢尾花数据(花萼长/宽、花瓣长/宽各 50 样本)用 CMYK 空间转 RGB 呈现,山鸢尾因第 2 分量“洋红”偏大被线性分离。但完整聚类拿不到,得靠分量平面:30x30 网格、300x300 图下,山鸢尾在第 1/3/4 平面取最小、第 2 平面取最大,和 CMYK 图互证。 通用版 SOM.mq5 能从 food.csv 读任意维数据,ColorScheme 支持 0/1/2/4 四种渐变(黑-白、蓝绿黄红、红黑绿、红白绿)。把维度解耦后,外汇贵金属玩家也能依此把多品种波动率向量化——不过多资产相关性受宏观事件驱动,高杠杆下误判聚类会引发快速回撤,仅作结构参考。

用自组织映射拆解优化结果的空间结构

在 MT5 策略测试器里跑完 MovingAverage.mq5 的遗传算法优化后,把 EURUSD H1、2011.01.01-2011.05.24 区间、以最大余额为目标的 400 组参数导出成 XML,再整理出 optim.csv 的 9 列数据(含获利、获利系数、回收系数、夏普比率、交易次数、获利交易百分比、移动周期、移动偏移)。用 som.mq5 生成的分量平面里,获利/获利系数/回收系数/夏普比率这四张图高度同向,最佳区落在左上角,对应 MovingPeriod 标记值约 45–50。 EA 按小时线测试、策略依赖趋势穿越,那个 45–50 的周期约等于 2 个交易日,可看作 2011 上半年欧美的“趋势记忆长度”。把移动周期调小,穿越变多、交易次数平面转黄红;移动偏移加大,交易次数反而走低,且它对收益类分量平面几乎无牵引——说明这参数不是该系统的敏感项。外汇与贵金属杠杆高、滑点跳空频繁,这类映射结论只描述历史样本内的概率倾向,实盘须重测。 下面这段 CSOMNode 类是承载单个神经元坐标与权重的底盘,读懂它就能改网格尺寸或权重维度去适配自己的 csv。 代码逐行看:类头声明 CSOMNode;protect 里 m_x1/m_y1/m_x2/m_y2 是节点在网格里的矩形边界整型坐标,m_x/m_y 是中心浮点坐标,m_weights[] 存该节点的特征权重向量;public 里构造析构成对出现,InitNode 用四个整型参数初始化边界,X() 是只读返回中心横标的内联方法。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSOMNode class                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSOMNode
  {
class="kw">protected:
   class="type">int               m_x1;
   class="type">int               m_y1;
   class="type">int               m_x2;
   class="type">int               m_y2;
   class="type">class="kw">double            m_x;
   class="type">class="kw">double            m_y;
   class="type">class="kw">double            m_weights[];
class="kw">public:
   class=class="str">"cmt">//--- class constructor
                     CSOMNode();
   class=class="str">"cmt">//--- class destructor
                    ~CSOMNode();
   class=class="str">"cmt">//--- node initialization
   class="type">void              InitNode(class="type">int x1,class="type">int y1,class="type">int x2,class="type">int y2);
   class=class="str">"cmt">//--- class="kw">return coordinates of the node&class="macro">#x27;s center
   class="type">class="kw">double            X()   const { class="kw">return(m_x);}

◍ CSOM 容器类把节点网格和训练参数收拢到一起

上文的 CSOMNode 只管单个神经元,真正跑自组织映射的是 CSOM 类。它内部用 m_som_nodes[] 动态数组装下全部节点,x、y 方向节点数由 m_xcells / m_ycells 控制,配合 m_xsize / m_ysize 决定输出位图尺寸。 训练相关的超参数都挂在 protected 区:m_map_radius 是邻域半径,m_time_constant 控制半径随迭代衰减的速度,m_initial_learning_rate 是初始学习率,m_iterations 是总迭代次数。这几个值直接决定网格收敛形态,外汇或贵金属行情特征做聚类时建议先用小样本调 m_iterations(例如 500~2000)观察节点分布是否稳定。 对外接口里,BestMatchingNode(double &vector[]) 返回与输入向量距离最近的节点下标,是推理时定位价格状态的核心方法;InitParameters(...) 在训练前必须调用,否则 m_som_nodes[] 未分配空间会触发数组越界。打开 MT5 把下面声明贴进头文件,就能接着写训练循环。

MQL5 / C++
class="type">class="kw">double Y() const { class="kw">return(m_y);}
class=class="str">"cmt">//--- returns the node coordinates
class="type">void GetCoordinates(class="type">int &x1,class="type">int &y1,class="type">int &x2,class="type">int &y2);
class=class="str">"cmt">//--- returns the value of weight_index component of weight&class="macro">#x27;s vector
class="type">class="kw">double GetWeight(class="type">int weight_index);
class=class="str">"cmt">//--- returns the squared distance between the node weights and specified vector
class="type">class="kw">double CalculateDistance(class="type">class="kw">double &vector[]);
class=class="str">"cmt">//--- adjust weights of the node
class="type">void AdjustWeights(class="type">class="kw">double &vector[],class="type">class="kw">double learning_rate,class="type">class="kw">double influence);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CSOM class                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSOM
  {
class="kw">protected:
  class=class="str">"cmt">//--- class for using of bmp images
  cIntBMP            m_bmp;
  class=class="str">"cmt">//--- grid mode
  class="type">int                m_gridmode;
  class=class="str">"cmt">//--- bmp image size
  class="type">int                m_xsize;
  class="type">int                m_ysize;
  class=class="str">"cmt">//--- number of nodes
  class="type">int                m_xcells;
  class="type">int                m_ycells;
  class=class="str">"cmt">//--- array with nodes
  CSOMNode           m_som_nodes[];
  class=class="str">"cmt">//--- total items in training set
  class="type">int                m_total_training_sets;
  class=class="str">"cmt">//--- training set array
  class="type">class="kw">double             m_training_sets_array[];
class="kw">protected:
  class=class="str">"cmt">//--- radius of the neighbourhood(used for training)
  class="type">class="kw">double             m_map_radius;
  class=class="str">"cmt">//--- time constant(used for training)
  class="type">class="kw">double             m_time_constant;
  class=class="str">"cmt">//--- initial learning rate(used for training)
  class="type">class="kw">double             m_initial_learning_rate;
  class=class="str">"cmt">//--- iterations(used for training)
  class="type">int                m_iterations;
class="kw">public:
  class=class="str">"cmt">//--- class constructor
                     CSOM();
  class=class="str">"cmt">//--- class destructor
                    ~CSOM();
  class=class="str">"cmt">//--- net initialization
  class="type">void              InitParameters(class="type">int iterations,class="type">int xcells,class="type">int ycells,class="type">int bmpwidth,class="type">int bmpheight);
  class=class="str">"cmt">//--- finds the best matching node, closest to the specified vector
  class="type">int               BestMatchingNode(class="type">class="kw">double &vector[]);

常见问题

先在代码里定义好输入向量的维度(比如收盘价、波动率等特征),再用 CSOM 容器类初始化节点网格和训练参数,最后把历史数据喂进去训练。
它本质是无监督降维,能把高维行情特征摊成二维邻域图,让你直观看到哪些品种或时段的形态在空间上靠得近。
小布可自动拉取对应品种特征跑 SOM 降维,把邻域聚类图直接画在品种页,你只看哪几块区域密集就行。
训练完后看网格上获胜节点分布,若某片区域节点被同类回测结果频繁激活,说明这些参数在空间上结构相近。
重点设网格边长、学习率和邻域半径衰减步长,初始学习率别太高,半径随迭代线性收缩通常更稳。