在 MetaTrader 中使用神经网络

Mariusz Woloszyn | 12 四月, 2016

简介

你们中很多人可能已考虑过在你们的 EA 中使用神经网络的可能性。 这个主题非常热门,尤其是在 2007 年自动交易锦标赛上,Better 以其基于神经网络的系统横扫对手之后,更是炙手可热。 很多互联网论坛充斥着与神经网络和外汇交易相关的主题。 然而遗憾的是,编写神经网络的本机 MQL4 实现并不简单。 它要求一些编程技巧,而且结果并不会很有效,尤其是当你想要在测试程序中针对大量数据测试你的最终结果时。

本文中,我将展示如何在你的 MQL4 代码中使用免费(在 LGPL 下)的著名快速人工神经网络库 (FANN),同时避免某些障碍和限制。 我进一步假设读者熟悉人工神经网络 (ann) 和与此主题相关的术语,因此我会侧重介绍使用以 MQL4 语言编写的人工神经网络的具体实现的实践性环节。



FANN 功能

为了充分了解 FANN 实现的可能性,读者需要熟悉其文档和最常用的函数。 FANN 的典型用例是创建一个简单的前馈网络,用一些数据训练此网络并运行它。 然后可将已创建并训练过的网络保存到文件中,之后可恢复以备进一步使用。 要创建一个人工神经网络,用户必须使用 fann_create_standard() 函数。 我们来看看它的语法:

FANN_EXTERNAL struct fann *FANN_API fann_create_standard(unsigned int num_layers, int lNnum, ... )

其中, num_layers 代表总层数,包括输入和输出层。 lNnum 和以下参数代表各个层(从输入层开始,以输出层结束)中的神经元数量。 要创建一个包含一个隐藏层且层中有 5 个神经元、10 个输入和 1 个输出的网络,用户必须进行如下调用:

fann_create_standard(3,10,5,1);

一旦创建了人工神经网络,下一步操作就是使用一些输入和输出数据对其进行训练。 最简单的训练方法是增量训练,可通过以下函数执行:

FANN_EXTERNAL void FANN_API fann_train(
        struct  fann    *       ann,

        
        fann_type       *       input,

        
        fann_type       *       desired_output  )

此函数将指针指向之前由 fann_create_standard() 返回的 struct fann 以及输入数据向量和输出数据向量。 输入和输出向量是 fann_type 类型的数组。 该类型事实上是 doublefloat 类型,具体取决于 FANN 的编译方式。 在此实现中,输入和输出向量都将成为 double 数组。

一旦人工神经网络训练完成,下一个期望功能将是运行该网络。 函数实现定义如下:

FANN_EXTERNAL fann_type * FANN_API fann_run(    struct  fann    *       ann,

        
        fann_type       *       input   )

此函数将指针指向 struct fann ,代表之前创建的网络和已定义类型(double 数组)的输入向量。 返回的值是一个输出向量数组。 这个事实是非常重要的,对于一个输出网络,我们始终获得一个带输出值的元素数组,而不是输出值本身。

遗憾的是,大部分 FANN 函数使用指向 struct fann 的指针, 代表无法直接由不支持结构作为数据类型的 MQL4 处理的人工神经网络 为了避免该限制,我们必须用某个方法进行一些包装,绕开 MQL4。 最简单的方法是创建一个用于保存正确值的 struct fann 指针数组,并用 int 变量表示的索引来参考它们。 用这种方法,我们可以将不受支持的变量类型替换为受支持类型,并创建一个可以轻松与 MQL4 代码整合的包装库。



包装 FANN

据我所知,MQL4 不支持带变量参数列表的函数,所以我们还得解决这个问题。 另一方面,如果调用了带过多参数的 C 函数(变量参数长度)且并未发生任何问题,那么我们可以假定 MQL4 函数中有固定的最大参数数量被传递到 C 库。 结果包装函数如下所示:

/* Creates a standard fully connected backpropagation neural network.
* num_layers - The total number of layers including the input and the output layer.
* l1num - number of neurons in 1st layer (inputs)
* l2num, l3num, l4num - number of neurons in hidden and output layers (depending on num_layers).
* Returns:
* handler to ann, -1 on error
*/

int __stdcall f2M_create_standard(unsigned int num_layers, int l1num, int l2num, int l3num, int l4num);

我们将先导的 fann_ 更改为 f2M_ (表示 FANN TO MQL),使用静态的参数数量(4 层),返回值现在是人工神经网络的内部数组的索引,其中包含 FANN 操作所需的 struct fann 数据。 用这种方法,我们可以轻松从 MQL 代码中调用此类函数。

同样的方式适用于以下情况:

/* Train one iteration with a set of inputs, and a set of desired outputs.
* This training is always incremental training, since only one pattern is presented.
* ann - network handler returned by f2M_create_*
* *input_vector - array of inputs
* *output_vector - array of outputs
* Returns:
* 0 on success and -1 on error
*/

int __stdcall f2M_train(int ann, double *input_vector, double *output_vector);

/* Run fann network
* ann - network handler returned by f2M_create_*
* *input_vector - array of inputs
* Returns:
* 0 on success, negative value on error
* Note:
* To obtain network output use f2M_get_output().
* Any existing output is overwritten
*/

int __stdcall f2M_run(int ann, double *input_vector);

最后一点也非常重要,即你应该通过以下调用清除你曾创建的人工神经网络:

/* Destroy fann network
* ann - network handler returned by f2M_*
* Returns:
* 0 on success -1 on error
* WARNING: the ann handlers cannot be reused if ann!=(_ann-1)
* Other handlers are reusable only after the last ann is destroyed.
*/

int __stdcall f2M_destroy(int ann);

要释放人工神经网络句柄,你应该以与网络创建顺序相反的顺序清除网络。 或者可以使用以下方式:

/* Destroy all fann networks
* Returns:
* 0 on success -1 on error
*/
int __stdcall f2M_destroy_all_anns();

但我很肯定,你们有些人可能更愿意保存已训练过的网络以留待后用,如下所示:

/* Save the entire network to a configuration file.
* ann - network handler returned by f2M_create*
* Returns:
* 0 on success and -1 on failure
*/
int __stdcall f2M_save(int ann,char *path);

当然,以后可以通过以下代码加载(更确切地说是重新创建)保存的网络:

/* Load fann ann from file
* path - path to .net file
* Returns:
* handler to ann, -1 on error
*/
int __stdcall f2M_create_from_file(char *path);

一旦我们了解了基本函数,我们就可以在我们的 EA 中试着使用它们,但首先我们需要安装 Fann2MQL 包。



安装 Fann2MQL

为了便于使用此包,我创建了 msi 安装程序,内含所有源代码和预编译库以及声明所有 函数的 Fann2MQL.mqh 头文件。

安装程序相当简单。 首先你会被告知,Fann2MQL 拥有 GPL 许可:


Fann2MQL 安装步骤 1

然后选择要安装此包的文件夹。 可以使用默认的 Program Files\Fann2MQL\ 或直接安装到你的 Meta Trader\experts\ 目录。 后者将把所有文件直接放到它们的位置,否则你必须手动复制它们。


Fann2MQL 安装步骤 2

安装程序将文件放到以下文件夹:


include\ folder


libraries\ folder


src\ folder

如果选择安装到 Fann2MQL 专用文件夹,请将其 includelibraries 子文件夹的内容复制到你 Meta Trader 的相应目录下。

安装程序还会将 FANN 库安装到你的系统库文件夹内(大部分情况下是 Windows\system32)。 src 文件夹 包含 Fann2MQL 的所有源代码。 如果需要了解内部构件的更多信息,可以阅读源代码,它是根本性的文档。 如果愿意的话,也可改进这些代码并添加其他功能。 如果你实现了什么有趣的东西,我鼓励你把你的包发给我。



在你的 EA 中使用神经网络

一旦安装了 Fann2MQL,你就可以开始编写你自己的 EA 或指标。 神经网络有大量可能的应用。 你可用它们预测未来的价格变动,但此类预测的质量以及真正充分利用其优势的可能性令人存疑。 你可试着用强化学习(也称 Q 学习或类似说法)技巧编写你自己的策略。 你可试着将神经网络用作你探试性 EA 的信号过滤器,或将所有这些技巧组合起来,再加上你真正想要的东西。 想象力是你唯一的束缚。

这里我将介绍一个例子,即将神经网络用作一个简单过滤器,以过滤 MACD 生成的信号。 请勿将其视为有价值的 EA,而应视为 Fann2MQL 的一个示例应用。 在说明示例 EA: NeuroMACD.mq4 的工作方式时,我会展示如何在 MQL 中有效利用 Fann2MQL。

每个 EA 的第一件事都是声明全局变量,定义和包括部分。 以下是包含这些内容的 NeuroMACD 的开头部分:

// Include Neural Network package
#include <Fann2MQL.mqh>

// Global defines
#define ANN_PATH "C:\\ANN\\"
// EA Name
#define NAME "NeuroMACD"

//---- input parameters
extern double Lots=0.1;
extern double StopLoss=180.0;
extern double TakeProfit=270.0;
extern int FastMA=18;
extern int SlowMA=36;
extern int SignalMA=21;
extern double Delta=-0.6;
extern int AnnsNumber=16;
extern int AnnInputs=30;
extern bool NeuroFilter=true;
extern bool SaveAnn=false;
extern int DebugLevel=2;
extern double MinimalBalance=100;
extern bool Parallel=true;

// Global variables

// Path to anns folder
string AnnPath;

// Trade magic number
int MagicNumber=65536;

// AnnsArray[ann#] - Array of anns
int AnnsArray[];

// All anns loded properly status
bool AnnsLoaded=true;

// AnnOutputs[ann#] - Array of ann returned returned
double AnnOutputs[];

// InputVector[] - Array of ann input data
double InputVector[];

// Long position ticket
int LongTicket=-1;

// Short position ticket
int ShortTicket=-1;

// Remembered long and short network inputs
double LongInput[];
double ShortInput[];

包括命令可加载 Fann2MQL.mqh 头文件,此文件中包含所有 Fann2MQL 函数的声明。 之后,所有 Fann2MQL 包函数都可在脚本中使用。 ANN_PATH 常数 定义使用已训练的 FANN 网络存储和加载文件的路径。 你需要创建该文件夹,即 C:\ANN。 NAME 常数 包含此 EA 的名称,它将在稍后用于加载和保存网络文件。 输入参数很是明显,那些不那么明显的会在后文中说明,全局变量也同样如此。

每个 EA 的切入点是其 init() 函数:

int init()
  {
   int i,ann;

   if(!is_ok_period(PERIOD_M5)) 
     {
      debug(0,"Wrong period!");
      return(-1);
     }

   AnnInputs=(AnnInputs/3)*3; // Make it integer divisible by 3

   if(AnnInputs<3) 
     {
      debug(0,"AnnInputs too low!");
     }
// Compute MagicNumber and AnnPath
   MagicNumber+=(SlowMA+256*FastMA+65536*SignalMA);
   AnnPath=StringConcatenate(ANN_PATH,NAME,"-",MagicNumber);

// Initialize anns
   ArrayResize(AnnsArray,AnnsNumber);
   for(i=0;i<AnnsNumber;i++) 
     {
      if(i%2==0) 
        {
         ann=ann_load(AnnPath+"."+i+"-long.net");
           } else {
         ann=ann_load(AnnPath+"."+i+"-short.net");
        }
      if(ann<0)
         AnnsLoaded=false;
      AnnsArray[i]=ann;
     }
   ArrayResize(AnnOutputs,AnnsNumber);
   ArrayResize(InputVector,AnnInputs);
   ArrayResize(LongInput,AnnInputs);
   ArrayResize(ShortInput,AnnInputs);

// Initialize Intel TBB threads
   f2M_parallel_init();

   return(0);
  }

首先,它检查 EA 是否应用于正确的时间范围。 AnnInputs 变量包含神经网络输入数。 由于我们将使用 3 组不同的参数,所以我们希望它能被 3 除尽。 计算 AnnPath 以反映 EA NAME MagicNumber,它是根据 SlowMAFastMASignalMA 输入参数(这些参数稍后将用于 MACD 指标信号)计算得出的。 一旦它知道了 AnnPath,EA 会尝试使用 ann_load() 函数(下文会介绍此函数)加载神经网络。 已加载网络中,一半用于执行多头仓过滤,另一半执行空头仓过滤。 AnnsLoaded 变量用于表明所有网络都执行了正确的初始化。 你可能已经注意到,此示例 EA 正在尝试加载多个网络。 我不确定此行为在此应用中是否必要,但我想向你展示 Fann2MQL 的全部潜力,它同时可处理多个网络,并可利用多个核心和 CPU 实现并行处理。 为此,Fann2MQL 利用了 Intel® 线程构建模块 技术。 函数 f2M_parallel_init() 用于初始化该接口。

以下是我初始化网络的方式:

int ann_load(string path)
  {
   int ann=-1;

   /* Load the ANN */
   ann=f2M_create_from_file(path);
   if(ann!=-1) 
     {
      debug(1,"ANN: '"+path+"' loaded successfully with handler "+ann);
     }
   if(ann==-1) 
     {

      /* Create ANN */
      ann=
          f2M_create_standard(4,AnnInputs,AnnInputs,AnnInputs/2+1,1);
      f2M_set_act_function_hidden(ann,FANN_SIGMOID_SYMMETRIC_STEPWISE);
      f2M_set_act_function_output(ann,FANN_SIGMOID_SYMMETRIC_STEPWISE);
      f2M_randomize_weights(ann,-0.4,0.4);
      debug(1,"ANN: '"+path+"' created successfully with handler "+ann);
     }
   if(ann==-1) 
     {
      debug(0,"INITIALIZING NETWORK!");
     }
   return(ann);
  }

如你所见,如果 f2M_create_from_file() 失败(即返回值为负值),则使用 f2M_create_standard() 函数创建网络,并用参数指示所创建的网络应有 4 个层(包括输入和输出层)、AnnInput 输入、第一个隐藏层中的 AnnInput 神经元、第二个隐藏层中的 AnnInput/2+1 神经元和输出层中的 1 个神经元。 f2M_set_act_function_hidden() 用于将隐藏层的激活函数设置为 SIGMOID_SYMMETRIC_STEPWISE(请参考 fann_activationfunc_enum 的 FANN 文档),并对输出层执行同样的操作。 然后调用 f2m_randomize_weights() ,用于初始化网络中的神经元连接权值。 这里我用了 <-0.4; 0.4> 的范围,但你可根据具体应用使用任何其他范围。

这时你可能已经发现我用了好几次 debug() 函数。 要改变你 EA 的详细级别,这是最简单的方法之一。 你可将此函数与输入参数 DebugLevel 配合使用,调整你代码生成调试输出的方式。

void debug(int level,string text)
  {
   if(DebugLevel>=level) 
     {
      if(level==0)
         text="ERROR: "+text;
      Print(text);
     }
  }

如果 debug() 函数的第一个参数调试 level 高于 DebugLevel ,则此函数不生成任何输出。 如果是低于或等于,则将打印出 text 字符串。 如果调试级别为 0,则会在开头添加字符串“ERROR: ”。 这样你就可以将你代码生成的调试划分为多个级别。 最重要的是可能发生的错误,因此它们被分配到级别 0。 它们会被打印出来,除非你将你的 DebugLevel 降至 0 以下(不建议这么做)。 1 级时会打印一些重要信息,例如网络成功加载或创建的确认信息。 2 级或以上时,打印内容的重要性会逐渐降低。

在详细说明 start() 函数之前(篇幅会较长),我需要介绍另外一些函数,用于准备网络输入和运行实际网络:

void ann_prepare_input()
  {
   int i;

   for(i=0;i<=AnnInputs-1;i=i+3) 
     {
      InputVector[i]=
         10*iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                  MODE_MAIN,i*3);
      InputVector[i+1]=
         10*iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                  MODE_SIGNAL,i*3);
      InputVector[i+2]=InputVector[i-2]-InputVector[i-1];
     }
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double ann_run(int ann,double &vector[])
  {
   int ret;
   double out;
   ret=f2M_run(ann,vector);
   if(ret<0) 
     {
      debug(0,"Network RUN ERROR! ann="+ann);
      return(FANN_DOUBLE_ERROR);
     }
   out=f2M_get_output(ann,0);
   debug(3,"f2M_get_output("+ann+") returned: "+out);
   return(out);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int anns_run_parallel(int anns_count,int &anns[],double &input_vector[])
  {
   int ret;

   ret=f2M_run_parallel(anns_count,anns,input_vector);

   if(ret<0) 
     {
      debug(0,"f2M_run_parallel("+anns_count+") returned: "+ret);
     }
   return(ret);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void run_anns()
  {
   int i;

   if(Parallel) 
     {
      anns_run_parallel(AnnsNumber,AnnsArray,InputVector);
     }

   for(i=0;i<AnnsNumber;i++) 
     {
      if(Parallel) 
        {
         AnnOutputs[i]=f2M_get_output(AnnsArray[i],0);
           } else {
         AnnOutputs[i]=ann_run(AnnsArray[i],InputVector);
        }
     }
  }
//+------------------------------------------------------------------+

函数 ann_prepare_input() 用于准备网络的输入名称(故此而得名)。 其用途很简单,但有一点必须注意,即输入数据必须进行正确的标准化。 这种情况下的标准化并不复杂,我只用了 MACD 主要值和信号值,这些值绝不会超出相关数据的所需范围。 在真实案例中,你也许应该更加关注这个问题。 你可能已经猜到过,选择网络输入的正确输入参数、进行编码、分解以及标准化正是神经网络处理的最重要因素之一。

正如前面所说,Fann2MQL 能够扩展 MetaTrader 的正常功能,即神经网络的并行多线程处理。 全局参数 Parallel 控制此行为。 run_anns() 函数 运行所有已初始化的网络,并获取它们的输出,然后存储到 AnnOutput[] 数组中。 AnnOutput[] 数组。 anns_run_parallel 函数负责以多线程方式处理任务。 它调用 f2m_run_parallel() ,此函数将待处理网络数量视为第一个参数,第二个参数是一个包含要运行的所有网络的句柄的数组, 并提供输入向量作为第三个参数。 所有网络必须针对相同的输入数据运行。 通过多次调用 f2m_get_output() 从网络中获取输出。

现在让我们看一看 start() 函数:

int
start()
  {
   int i;
   bool BuySignal=false;
   bool SellSignal=false;

   double train_output[1];

   /* Is trade allowed? */
   if(!trade_allowed()) 
     {
      return(-1);
     }

   /* Prepare and run neural networks */
   ann_prepare_input();
   run_anns();

   /* Calulate last and previous MACD values.
* Lag one bar as current bar is building up
*/
   double MacdLast=iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                         MODE_MAIN,1);
   double MacdPrev=iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                         MODE_MAIN,2);

   double SignalLast=iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                           MODE_SIGNAL,
                           1);
   double SignalPrev=iMACD(NULL,0,FastMA,SlowMA,SignalMA,PRICE_CLOSE,
                           MODE_SIGNAL,
                           2);

   /* BUY signal */
   if(MacdLast>SignalLast && MacdPrev<SignalPrev) 
     {
      BuySignal=true;
     }
   /* SELL signal */
   if(MacdLast<SignalLast && MacdPrev>SignalPrev) 
     {
      SellSignal=true;
     }

   /* No Long position */
   if(LongTicket==-1) 
     {
      /* BUY signal */
      if(BuySignal) 
        {
         /* If NeuroFilter is set use ann wise to decide :) */
         if(!NeuroFilter || ann_wise_long()>Delta) 
           {
            LongTicket=
          OrderSend(Symbol(),OP_BUY,Lots,Ask,3,
                    Bid-StopLoss*Point,
                    Ask+TakeProfit*Point,
                    NAME+"-"+"L ",MagicNumber,0,Blue);
           }
         /* Remember network input */
         for(i=0;i<AnnInputs;i++) 
           {
            LongInput[i]=InputVector[i];
           }
        }
        } else {
      /* Maintain long position */
      OrderSelect(LongTicket,SELECT_BY_TICKET);
      if(OrderCloseTime()==0) 
        {
         // Order is opened
         if(SellSignal && OrderProfit()>0) 
           {
            OrderClose(LongTicket,Lots,Bid,3);
           }
        }
      if(OrderCloseTime()!=0) 
        {
         // Order is closed
         LongTicket=-1;
         if(OrderProfit()>=0) 
           {
            train_output[0]=1;
              } else {
            train_output[0]=-1;
           }
         for(i=0;i<AnnsNumber;i+=2) 
           {
            ann_train(AnnsArray[i],LongInput,train_output);
           }
        }
     }

   /* No short position */
   if(ShortTicket==-1) 
     {
      if(SellSignal) 
        {
         /* If NeuroFilter is set use ann wise to decide ;) */
         if(!NeuroFilter || ann_wise_short()>Delta) 
           {
            ShortTicket=
          OrderSend(Symbol(),OP_SELL,Lots,Bid,3,
                    Ask+StopLoss*Point,
                    Bid-TakeProfit*Point,NAME+"-"+"S ",
                    MagicNumber,0,Red);
           }
         /* Remember network input */
         for(i=0;i<AnnInputs;i++) 
           {
            ShortInput[i]=InputVector[i];
           }
        }
        } else {
      /* Maintain short position */
      OrderSelect(ShortTicket,SELECT_BY_TICKET);
      if(OrderCloseTime()==0) 
        {
         // Order is opened
         if(BuySignal && OrderProfit()>0) 
           {
            OrderClose(LongTicket,Lots,Bid,3);
           }
        }
      if(OrderCloseTime()!=0) 
        {
         // Order is closed
         ShortTicket=-1;
         if(OrderProfit()>=0) 
           {
            train_output[0]=1;
              } else {
            train_output[0]=-1;
           }
         for(i=1;i<AnnsNumber;i+=2) 
           {
            ann_train(AnnsArray[i],ShortInput,train_output);
           }
        }
     }

   return(0);
  }
//+------------------------------------------------------------------+

我只简单的介绍一下,因为注释里已经有很详尽的说明了。 run_anns() () 检查是否允许交易。 基本上, 它检查 AnnsLoaded 变量指示所有人工神经网络都正确进行了初始化,然后检查正确的时间范围周期最小帐户余额,最后允许仅在新条柱上第一次价格变动时进行交易。 接下来的两个函数用于准备网络输入和执行网络处理,上面几行中已有说明。 下一步,我们计算和放入变量,以对最后一个形成的条柱及其前一个条柱的信号和主线的 MACD 值进行后续处理。 忽略当前条柱,因为它尚未形成,可能会重新绘制。 run_anns() MACD 信号主线交叉信号 来计算SellSignal 和 BuySignal。 这两个信号用于对称的多头仓和空头仓处理,因此我将仅说明多头仓的情况。

run_anns() 变量 包含当前已建仓位的编号数。 如果它等于 -1,则不建仓,因此如果设置了 主线交叉信号 ,可能就表示有建多头仓的好机会。 如果变量 NeuroFilter 未设置,则建立多头仓,并且这种情况下神经网络不对信号进行过滤 -- 发送的订单是买入订单。 此时, LongInput 变量会记住 ann_prepare_input() 准备的 InputVector ,以备后用。

如果 LongTicekt 变量包含有效编号数,EA 会检查订单是仍处于未结状态还是已被 StopLoss 或 TakeProfit 平仓。 如果订单仍未结,什么都不会发生,但如果已平仓,则将计算仅有一个输出的 train_output[] 向量,如果是亏损平仓,输出值为 -1,如果是获利平仓,输出值为 1。 然后将该值传递给 ann_train() 函数,所有负责处理多头仓的网络都将用它进行训练。 变量 LongInput 用作输入向量,它包含 ann_prepare_input() InputVector。 通知这种方式告知网络哪个信号可实现获利,哪个信号不行。

一旦你将已训练的网络从 NeuroFilter 切换到 true ,则启用网络过滤。 run_anns() ann_wise_long() ,所得值为所有用于处理多头仓的网络返回的值的平均值。 run_anns() 参数 作为一个阈值,指示过滤信号是否有效。像很多其他值一样,此阈值也是通过优化流程获取的。

现在,一旦我们了解了它的工作原理,我会向你展示它的应用。 测试货币对当然是 EURUSD。 我用的是来自 Alpari 的数据,转换至 M5 时间范围。 用于训练/优化的周期为 2007.12.31 至 2009.01.01,用于测试目的的周期为 2009.01.01 至 2009.03.22。 第一次运行时,我尝试获取 StopLoss、TakeProfit、SlowMA、FastMA 和 SignalMA 参数的最具盈利能力的值,然后用代码编写到 NeuroMACD.mq4 文件中。 NeuroFIlter SaveAnn 都被禁用, AnnsNumber 则设置为 0 以避开神经网络处理。 我使用了遗传算法进行优化。 获取值后,所获报告如下:


基本参数优化后的训练数据报告。

正如你所见,我在迷你帐户上运行了这个 EA,使用的是 0.01 手数和 200 的初始余额。 当然你也可以根据自己的帐户设置或偏好调整这些参数。

此时我们已有足够的获利和亏损交易数,所以我们可以启用 SaveAnn, 都被 并将 AnnsNumber 设为 30。= 30。 做完这些,我再次运行了测试程序。 结果除了一点以外,其他完全相同,这一点不同之处就是流程慢了不少(使用了神经网络处理的缘故),并且文件夹 C:\ANN 中满是已训练的网络,如下图中所示。 运行此流程之前,确保 C:\ANN 文件夹是存在的!


C:\\ANN\\ 文件夹。

训练了网络之后,就到了测试其表现的时候了。 首先我们将用训练数据来做一下测试。 将 NeuroFilter 切换到 true ,并将 SaveAnn 更改为 false ,然后启动测试程序。 所得结果显示在下方。 注意,你的测试结果可能会有少许不同,因为网络初始化过程中提供的神经元连接权值有一点随机性(此例中我明确调用了 ann_load() 内的 2M_randomize_weights()) 。


启用信号神经过滤时在训练数据上所获的结果。

净利润略有增长 (20.03 vs 16.92),但获利因子高了很多 (1.25 vs 1.1)。 交易数少了很多 (83 vs 1188),平均连续亏损数从 7 降至 2。 但这仅仅表明神经信号过滤正在发挥作用,却只字未提它对训练期间未使用的数据做了什么。 以下是从测试周期 (2009.01.01 - 2009.30.28) 中获取的结果:


启用神经过滤后从测试数据中获得的结果。

执行的交易数相当少,很难说这个策略的质量到底如何,但我并不是要教你如何编写最具盈利能力的 EA,而是说明如何在你的 MQL4 代码中使用神经网络。 这种情况下,要看到使用神经网络的实际效果,只有将启用和禁用 NeuroFilter 时 EA 在测试数据上所得的结果进行比较。 以下是未采用神经信号过滤时从测试数据周期中获得的结果:


无神经过滤时测试数据所得结果

差异相当明显。 可以看到,神经信号过滤将亏损 EA 转变成了获利 EA!



总结

我希望你从本文中学会了如何在 MetaTrader 中使用神经网络。 借助简单的免费开源包 Fann2MQL,你可把神经网络层添加到几乎任何 Expert Advisor 中,或开始尝试编写自己的完全或部分基于神经网络的 Expert Advisor。 独有的多线程功能可以大大加快你的处理速度(尤其是在优化某些参数时),具体取决于你的 CPU 内核数量。 在一个案例中,它缩短了我的强化学习的优化时间,以 4 核 Intel CPU 为基础,将 EA 处理时间从 4 天左右缩短至“仅”28 小时。

撰写本文时,我决定把 Fann2MQL 放到其自己的网站上去: http://fann2mql.wordpress.com/。 在那里你可以找到 Fann2MQL 的最新版本和所有可能的未来版本,以及所有函数的文档。 我承诺将此软件的所有版本都归于 GPL 许可之下,因此如果你发给我任何我觉得有趣的注释、功能请求或补丁,那就肯定能在后续版本中看到它们。

请注意,本文仅介绍 Fann2MQL 的一些非常基本的用法。 因为此软件包并不比 FANN 复杂多少,你可使用所有用于管理 FANN 网络的工具,例如:

快速人工神经网络库的主页上还有更多有关 FANN 的工具: http://leenissen.dk/fann/!



附言

写下这篇文章后,我找到了 NeuroMACD.mq4 的一个小错误。 用于空头仓的 OrderClose() 函数使用的是多头仓编号。 这导致策略发生倾斜,更可能在空头时保留仓位,而在多头时平仓:

/* Maintain short position */
OrderSelect(ShortTicket,SELECT_BY_TICKET);
if(OrderCloseTime()==0) 
  {
// Order is opened
   if(BuySignal && OrderProfit()>0) 
     {
      OrderClose(LongTicket,Lots,Bid,3);
     }
  }

在此脚本的正确版本中,我已经修复了这个问题,并完全移除了 OrderClose() 策略。 这不会改变神经过滤对 EA 的整体影响,但余额曲线形状会大有不同。 本文随附了此 EA 的这两个版本。