English Русский Español Deutsch 日本語 Português
preview
您应该知道的 MQL5 向导技术(第 02 部分):Kohonen 映射

您应该知道的 MQL5 向导技术(第 02 部分):Kohonen 映射

MetaTrader 5交易系统 | 10 十月 2022, 14:55
763 0
Stephen Njuki
Stephen Njuki

 

1. 概述    

1.1 继续有关 MQL5 向导的这一系列文章,我们将深入 Kohonen 映射 研究这个问题。 根据维基百科,这些技术用于生成高维数据集的低维(通常是二维)表示,同时保留数据的拓扑结构。 它们是由 Teuvo Kohonen 在 20 世纪 80 年代推广开来的

简单而言,kohonen 映射(又称自组织映射)可以在不丢失汇总内容清晰度的情况下归纳复杂性。 汇总是一种组织形式,因此被称为自组织。 经由重新组织的数据或映射,我们有两组相关数据。 原始的高维数据作为我们的输入,而汇总(低维数据)形式如常,但并不总是以二维表示作为输出 。 输入是已知的,而输出则是未知的,或者在这种情况下是“已学习”。

对于交易者来说,如果我们只关注基于时间的价格序列,那么任何时候的已知价格(我们称之为 feed 数据)都是该时刻左侧价格,而未知(我们称为 functor 数据)是右侧的价格。 我们如何针对已知和未知进行分类,决定了 feed 和 functor 数据两者的维数。 这是交易员应当关注的问题,因为他们对市场的展望和态度对这一点有很大的影响。  

1.2  这些映射的一个常见误解是 functor 数据应该是图像或二维。 下面的图片都是作为 Kohonen 映射的代表而被分享的。

typical_image

虽然没有错,但我想强调 functor 可以且也许应该(对于交易者)有一个单一维度。 因此,我们不会将高维数据降维到 2D 映射,而是将其映射到一条直线上。根据定义,Kohonen 映射旨在降低维度,因此我希望我们将这一点带到本文的下一个层面。kohonen 映射在层数和底层算法上都不同于常规神经网络。 它是神经元的单层(通常是前面提到的线性 2D 网格)集合,取代了多层。 这一层上的所有神经元,即我们正在参考的 functor 都与 feed 连接,但不是它们自身,这意味着神经元不受彼此权重的直接影响,且只随 feed 层面数据更新。functor 数据层通常是一个“映射”,它根据 feed 数据在每次训练迭代中组织自己。由此,训练之后,每个神经元在 functor 层中都有权重调整维度,这令我们可以计算任意两个这样的神经元之间的 Euclidean 距离。

 

2. 创建类

2.1  类结构

2.1.1 Dimension  是我们首先要定义的抽象类。 如果我把大部分代码放在一个单独的文件中,并简单地引用它,那么这段代码就会更整洁;不过我想还是放在下一篇文章中与资金和尾随类一起介绍这段代码,所以,目前还和上一篇文章一样,所有代码都放在信号文件当中。 维度在该网络中始终很重要,因为它们严重影响输出。 feed 数据(输入)通常是多维的。 functor 数据(输出)与典型的 x 和 y 对比有一维。 基于 feed 和 functor 两者数据的多维性,理想的数据类型是双精度数组。

然而,为了顺应探索 MQL5 函数库的趋势,我们采用双精度类型的数组列表。 feed 数据是在一根柱线的高度内,最低价的变化减去最高价的变化,就像我们在前一篇文章中所用的那样。 原则上,输入更好应根据交易员对市场的洞察力来选择,而不应由每个人在实盘或测试账户中所采用。 每名交易员都应该修改此代码,来适配自己的输入数据。 如前所述,functor 数据将是一维的。 不过,由于它也是一个列表,因此可以对其进行自定义,从而添加更多维度。 但出于我们的目的,我们将重点关注最近柱线的开盘价和收盘价之间的变化。 再次,MQL5 向导允许您依据所选时间帧来设置柱线。 维度类将继承自 MQL5 代码库清单中的双精度型接口。 在该类中添加两个函数,即 Get 和 Set。 顾名思义,一旦提供了索引,它们就有助于检索和设置列表中的值。

#include                        <Generic\ArrayList.mqh>
#include                        <Generic\HashMap.mqh>

#define                         SCALE 5

#define                         IN_WIDTH 2*SCALE
#define                         OUT_LENGTH 1

#define                         IN_RADIUS 100.0
#define                         OUT_BUFFER 10000

//
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cdimension                :  public CArrayList<double>
  {
    public:
        
      Cdimension()              {};
      ~Cdimension()             {};
        
     virtual double             Get(const int Index)                                    
                                {       
                                  double _value=0.0; TryGetValue(Index,_value); return(_value); 
                                };
     virtual void               Set(const int Index,double Value)       
                                {  
                                  Insert(Index,Value);                                                                                                                   
                                };
  };


2.1.2  Feed 类将继承上面刚刚创建的维度类。 此处不会添加特殊函数。 只有构造函数会指定列表容量(类似于数组大小),feed 数据列表的默认大小为 10。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cfeed             : public Cdimension
 {
   public:
                
     Cfeed()            { Clear(); Capacity(IN_WIDTH);  };
     ~Cfeed()           {                               };
 };


2.1.3  Functor 类与 feed 类类似,但需要注意的是其大小。 如上所述,我们将研究 functor 数据的一个(而不是通常的两个)维度,因此集合大小将为 1。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cfunctor          : public Cdimension
 {
   public:
                
   Cfunctor()           { Clear(); Capacity(OUT_LENGTH); };
   ~Cfunctor()          {                                };
 };


2.1.4   Neuron 类是我们从代码感到有之处。 我们将其声明为继承自 MQL5 函数库接口的类,该接口接受两个自定义数据类型。 一个关键字和一个数值。 所讨论的模板接口是 HashMap我们将在上面两个类中用到的自定义数据类型也要声明。 即 Feed 类作为关键字,Functor 类作为数值。 我们也没有函数,只有指向 Feed 类、Functor 类和 “关键字-数值” 类的指针。 顾名思义,这个类的目的是定义神经元。 神经元是我们的数据单位,因为它包括输入数据类型(feed 数据)和输出数据类型(functor 数据)。 它是一个神经元的 feed 数据,与已经训练好的神经元相匹配,以便投射出 functor 可能是什么。 映射的神经元也可以在新神经元训练时调整其 functor 数据。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cneuron           : public CHashMap<Cfeed*,Cfunctor*>
 {
   public:
                
    double              weight;
                        
    Cfeed               *fd;
    Cfunctor            *fr;
                        
    CKeyValuePair
    <
    Cfeed*,
    Cfunctor*
    >                   *ff;
                        
    Cneuron()           {
                          weight=0.0;
                          fd = new Cfeed();
                          fr = new Cfunctor();
                          ff = new CKeyValuePair<Cfeed*,Cfunctor*>(fd,fr);
                          Add(ff);
                        };
                                                                        
   ~Cneuron()           {
                          ZeroMemory(weight);
                          delete fd;
                          delete fr;
                          delete ff;
                        };
 };


2.1.5   Layer 抽象类如下所示。 它继承自 neuron 类的列表模板,并有一个对象 — neuron 指针。 作为一个抽象类,这个神经元指针由所继承的这个类来使用。 有两个这样的类,即输入层和输出层。 严格地说,Kohonen 映射不应归类为神经网络,因为它们没有带权重和反向传播的前馈链接。 不过,一些支持者认为它们只是一种不同的类型。 

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Clayer            : public CArrayList<Cneuron*>
 {
   public:
                
    Cneuron             *n;
                
    Clayer()            { n = new Cneuron();     };
    ~Clayer()           { delete n;              };
 };


2.1.6   输入层 类继承自抽象层类。 它是网络运行时存储实时和最近 feed 数据值的地方。它不是一个拥有多个神经元的典型层,而是一个拥有最新 feed 和 functor 数据的单个神经元,因此其大小将为 1。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cinput_layer      : public Clayer
 {
   public:
                
   static const int     size;
                        
    Cinput_layer()      {
                          Clear();
                          Capacity(Cinput_layer::size);
                          for(int s=0; s<size; s++)
                          {
                            n = new Cneuron();
                            Add(n);
                          }
                        }
    ~Cinput_layer()     {};
 };
 const int Cinput_layer::size=1;


2.1.7  输出层 类也继承自层类,但它作为我们的映射,因为“经过训练的”神经元会存储在这里。 这一层神经元的 functor 数据部分相当于典型 SOM 的图像或映射。它的大小最初将为 10000,并且将随着新神经元的训练而增加相同的数量

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Coutput_layer      : public Clayer
 {
   public:
                
    int                  index;
    int                  size;
                        
    Coutput_layer()      {
                           index=0;
                           size=OUT_BUFFER;
                           Clear();
                           Capacity(size);
                           for(int s=0; s<size; s++)
                           {
                             n = new Cneuron();
                             Add(n);
                           }
                         };
                                                                        
    ~Coutput_layer()     {
                           ZeroMemory(index);
                           ZeroMemory(size);
                         };
 };


2.1.8  网络 类就像 neuron 类一样也继承自 HashMap 模板接口。 它的关键字和数值数据类型是输入层类和输出层类。 它拥有最多的函数(9),不仅可以获取列表大小,还可以检索和更新各个层上的神经元。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cnetwork           : public CHashMap<Cinput_layer*,Coutput_layer*>
 {
   public:
                
     Cinput_layer        *i;
     Coutput_layer       *o;
                        
     CKeyValuePair
     <
     Cinput_layer*,
     Coutput_layer*
     >                   *io;
                        
     Cneuron             *i_neuron;
     Cneuron             *o_neuron;
                        
     Cneuron             *best_neuron;
                        
     Cnetwork()          {
                           i = new Cinput_layer();
                           o = new Coutput_layer();
                           io = new CKeyValuePair<Cinput_layer*,Coutput_layer*>(i,o);
                           Add(io);
                                                                                
                           i_neuron = new Cneuron();
                           o_neuron = new Cneuron();
                                                                                
                           best_neuron = new Cneuron();
                         };
                                                                        
     ~Cnetwork()         {
                           delete i;
                           delete o;
                           delete io;
                           delete i_neuron;
                           delete o_neuron;
                           delete best_neuron;
                         };
                        
      virtual int        GetInputSize()
                         {
                           TryGetValue(i,o);
                           return(i.size);
                         };
                        
      virtual int        GetOutputIndex()
                         {
                           TryGetValue(i,o);
                           return(o.index);
                         };
                        
      virtual void       SetOutputIndex(const int Index)
                         {
                           TryGetValue(i,o);
                           o.index=Index;
                           TrySetValue(i,o);
                         };
                        
      virtual int        GetOutputSize()
                         {
                           TryGetValue(i,o);
                           return(o.size);
                         };
                        
      virtual void       SetOutputSize(const int Size)
                         {
                           TryGetValue(i,o);
                           o.size=Size;
                           o.Capacity(Size);
                           TrySetValue(i,o);
                         };
                        
      virtual void       GetInNeuron(const int NeuronIndex)
                         {
                           TryGetValue(i,o);
                           i.TryGetValue(NeuronIndex,i_neuron);
                         };
                        
      virtual void       GetOutNeuron(const int NeuronIndex)
                         {
                           TryGetValue(i,o);
                           o.TryGetValue(NeuronIndex,o_neuron);
                         };
                        
      virtual void       SetInNeuron(const int NeuronIndex)
                         {
                           i.TrySetValue(NeuronIndex,i_neuron);
                         };
                        
      virtual void       SetOutNeuron(const int NeuronIndex)
                         {
                           o.TrySetValue(NeuronIndex,o_neuron);
                         };
 };


2.1.9   映射 类是最终的综合类。 它调用网络类的实例,并包含用于训练神经元和获取网络最佳匹配神经元的其它变量。 

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
class Cmap
  {
    public:
                        
      Cnetwork               *network;
                        
      static const double     radius;
      static double           time;
                        
      double                  QE;       //proxy for Quantization Error
      double                  TE;       //proxy for Topological Error
                        
      datetime                refreshed;
                        
      bool                    initialised;
                        
      Cmap()                  {
                                network = new Cnetwork();
                                                                                        
                                initialised=false;
                                                                                        
                                time=0.0;
                                                                                        
                                QE=0.50;
                                TE=5000.0;
                                                                                        
                                refreshed=D'1970.01.05';
                               };
                                                                                
      ~Cmap()                  {
                                 ZeroMemory(initialised);
                                                                                        
                                 ZeroMemory(time);
                                                                                        
                                 ZeroMemory(QE);
                                 ZeroMemory(TE);
                                                                                        
                                 ZeroMemory(refreshed);
                               };
 };
 const double Cmap::radius=IN_RADIUS;
 double Cmap::time=10000/fmax(1.0,log(IN_RADIUS));

 


2.2. 拓扑

2.2.1  神经元训练调整输出层中现有神经元的 functor 权重,并添加新训练神经元的竞争学习过程。 调整这些权重的比率,最重要的是调整这些权重所需的迭代次数,是判定网络效率的非常敏感的参数。 每次迭代调整权重时,都会计算一个新的较小半径。 我将此半径称为 functor 误差(不要与 SOM 拓扑误差混淆),但大多数人将其称为 Euclidean 距离测量的邻域半径。 我选择 “误差”,因为这是一个需要最小化的参数,以便获得更佳的网络结果。 迭代执行次数越多,Functor-误差越小。除了迭代次数,学习速率需要从接近一的数字逐渐降低到零

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSignalKM::NetworkTrain(Cmap &Map,Cneuron &TrainNeuron)
  {
    Map.TE=0.0;

    int _iteration=0;
    double _training_rate=m_training_rate;

    int _err=0;
    double _functor_error=0.0;

    while(_iteration<m_training_iterations)
    {
      double _current_radius=GetTrainingRadius(Map,_iteration);

      for(int i=0; i<=Map.network.GetOutputIndex(); i++)
      {
        Map.network.GetOutNeuron(i);
        double _error = EuclideanFunctor(TrainNeuron,Map.network.o_neuron);

        if(_error<_current_radius)
        {
          _functor_error+=(_error);
          _err++;

          double _remapped_radius = GetRemappedRadius(_error, _current_radius);

          SetWeights(TrainNeuron,Map.network.o_neuron,_remapped_radius,_training_rate);

          Map.network.SetOutNeuron(i);
        }
      }

      _iteration++;
      _training_rate=_training_rate*exp(-(double)_iteration/m_training_iterations);
    }

    int
    _size=Map.network.GetOutputSize(),
    _index=Map.network.GetOutputIndex();
    Map.network.SetOutputIndex(_index+1);
    if(_index+1>=_size)
    {
      Map.network.SetOutputSize(_size+OUT_BUFFER);
    }

    Map.network.GetOutNeuron(_index+1);
    for(int w=0; w<IN_WIDTH; w++)
    {
      Map.network.o_neuron.fd.Set(w,TrainNeuron.fd.Get(w));
    }
    
    for(int l=0; l<OUT_LENGTH; l++)
    {
      Map.network.o_neuron.fr.Set(l,TrainNeuron.fr.Get(l));
    }

    Map.network.SetOutNeuron(_index+1);

    if(_err>0)
    {
      _functor_error/=_err;
      Map.TE=_functor_error*IN_RADIUS;
    }
  }


2.2.2  拓扑误差是 Kohonen 映射中的一个关键属性。 我取其作为衡量输出层与其长期预期目标之间距离有多靠近。 记住,每次训练输出层神经元都会适配真实或预期的结果,所以问题就变成了我们如何衡量这一过程。 答案是,如果我们更多地保留输出层,那么我们就更接近这个目标。 出于本文的目的,我将让 functor 误差充当它的代理。


2.3. 量化

2.3.1   神经元映射是寻找只存在于 feed 数据,且最适合神经元的 functor 权重的过程。 这是通过在输出层中找到与神经元的 Euclidean 距离最短的 feed 数据来实现的,因为没有已知的 functor 数据。 与训练一样,我将此距离称为 feed 误差。同样,我们的值越小,网络就越可靠。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSignalKM::NetworkMapping(Cmap &Map,Cneuron *MapNeuron)
  {
    Map.QE=0.0;
    
    Map.network.best_neuron = new Cneuron();

    int _random_neuron=rand()%Map.network.GetOutputIndex();

    Map.network.GetInNeuron(0);
    Map.network.GetOutNeuron(_random_neuron);

    double _feed_error = EuclideanFeed(Map.network.i_neuron,Map.network.o_neuron);

    for(int i=0; i<Map.network.GetOutputIndex(); i++)
    {
      Map.network.GetOutNeuron(i);

      double _error = EuclideanFeed(Map.network.i_neuron,Map.network.o_neuron);

      if(_error < _feed_error)
      {
        for(int w=0; w<IN_WIDTH; w++)
        {
          Map.network.best_neuron.fd.Set(w,Map.network.o_neuron.fd.Get(w));
        }

        for(int l=0; l<OUT_LENGTH; l++)
        {
          Map.network.best_neuron.fr.Set(l,Map.network.o_neuron.fr.Get(l));
        }

        _feed_error = _error;
      }
    }

    Map.QE=_feed_error/IN_RADIUS;
}


2.3.2   量化误差是 Kohonen 映射中的另一个关键属性,在我印象中它没有一个简明的定义。 我认为这是将高维数据转换为低维输出的误差。 在我们的例子中,这里是将提要转换为 functor 误差。 在本文中,我将采用 feed 误差作为它的代理。


 

3. 利用 MQL5 向导装配

3.1  向导装配笔直朝前。 在此我唯一要注意的是,首先要在大时间帧内开始测试,因为理想情况下,若训练覆盖足够时段,10000 次训练迭代每根柱线都需要花费一些时间。

wizard_1

 


 

4. 在策略测试器里测试

4.1   我们测试的默认输入将调查量化误差代理(QE)和拓扑误差代理(TE)的灵敏度。 我们将查看这两个场景。 首先,我们将采用非常保守的数值进行测试,QE 和 TE 分别为 0.5 和 12.5;然后我们将在 0.75 和 25.0 分别测试这些输入。

criteria_1

保守选项


criteria_2

激进选项

 

 

输入不是太多。 我们有“训练读取”功能,可判定是否应在初始化之前读取训练文件。 如果该文件不存在,智能系统将不会验证。 我们还有“训练写入”功能,顾名思义,一旦智能系统逆初始化完成,它判定是否应该写入学习文件。 训练总是在智能系统运行时进行。 在“training only” 输入参数里设置选项:仅训练且无交易 。 Kohonen 映射的另外两个重要参数是 “training rate”(也称为学习率)和训练迭代。 通常,这两个值越高(学习率上限为 1.0),性能就越好,但这需要花费时间和 CPU 资源。 

智能系统基于 2018.10.01 至 2021.06.01 区间的 EURJPY 进行了训练,并从训练结束日期至目前为期进行了前向验证测试。

本报告是采用保守选项:

report_1

此为净值曲线:

curve_1


不过,此报告是更激进的选项:

report_2

 

此为净值曲线:

curve_2

 

很清楚,需要对风险和持仓规模进行更多的测试和微调,但对于在如此短的区间内接受训练的系统来说,这是很有希望的。 然而,对比上述两种场景,鉴于锋锐比率值 0.43 几乎是更多交易 0.85 的一半,似乎更保守的选项并没有得到足够的回报。 除了根据个人交易风格定制 feed 和 functor 数据之外,在使用之前,还需要进行更多的研究;在部署之前,应该在相当长的一段时间内基于经纪商的实际跳价数据进行初步测试。

 

5. 结束语

5.1   MQL5 向导是显然一款非常灵活的工具,能在时间紧迫的情况下装配交易系统。 对于本文,我们探讨了 Kohonen 映射的选项,该映射将价格时间序列的多维 feed 数据转换到范围从 -1.0 到 1.0 的单一维度。 虽然这不常见,但这种方式确实捍卫 Kohonen 映射的本质,即降低复杂性和简化决策。 我们还展示了 MQL 函数库中的更多代码,如数组列表哈希映射。 我希望您喜欢它。 感谢阅读!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/11154

附加的文件 |
价格走势模型及其主要规定(第 1 部分):概率价格域演化方程与发生的可观测随机游走 价格走势模型及其主要规定(第 1 部分):概率价格域演化方程与发生的可观测随机游走
本文研究的是概率价格域演化方程,与即将到来的价格尖峰准则。 它还揭示了图表上价格数值的本质,以及这些数值随机游走的发生机制。
从头开始开发智能交易系统(第 22 部分):新订单系统 (V) 从头开始开发智能交易系统(第 22 部分):新订单系统 (V)
今天,我们将继续开发新订单系统。 实现一个新系统并非那么容易,因为我们经常会遇到各种问题令过程复杂化。 当这些问题出现时,我们必须停下来重新分析我们前进的方向。
DoEasy. 控件 (第 10 部分): WinForms 对象 — 动画界面 DoEasy. 控件 (第 10 部分): WinForms 对象 — 动画界面
现在是时候实现动画图形界面功能,方便用户与对象的交互了。 为了让更复杂的对象能正确工作,还需要新功能。
学习如何基于 Williams PR 设计交易系统 学习如何基于 Williams PR 设计交易系统
本系列中的一篇新文章,介绍了如何依据 MQL5 最流行的技术指标为 MetaTrader 5 设计交易系统。 在本文中,我们将学习如何依据 Williams‘ %R 指标设计交易系统。