下载MetaTrader 5

MQL5 Cookbook: 使用不限数量的参数开发多币种EA交易

7 十一月 2013, 12:14
Anatoli Kazharski
0
949

简介

在前一篇文章"MQL5 Cookbook: 多币种EA交易 - 简介而快速的途径"中的EA交易。如果交易品种和交易策略参数的数量较小的情况下可能很有用,然而,MQL5中EA交易的输入参数有数量的限制: 它们不能超过1024个。

还有,尽管这个数量基本够用了,但是使用这样巨大的参数列表还是不大方便,每次需要为某个交易品种修改或者优化参数的时候,您都需要在长长的参数列表中为该交易品种找到它的参数。

您可以自己看一下客户终端帮助中输入参数部分找到其他限制。

在本文中,我们将创建一种模式,它会使用一系列参数为交易系统作优化,而且允许不加数量限制的参数。交易品种的列表将在标准文本文件 (*.txt) 中创建,每个交易品种的输入参数也将存储于文件中。

在此需要说明的是,尽管在正常模式下这个EA交易处理一个交易品种,然而您可以在策略测试器中选择各种交易品种进行测试(每个交易品种都是独立的)。

事实上,直接在市场报价窗口中创建交易品种列表会更加方便,它使您可以保存准备好的交易品种集合,我们会让此EA交易在策略测试器中的市场报价窗口直接取得交易品种列表。可惜的是现在无法从策略测试器中访问市场报价窗口,所以我们必须人工或者使用脚本来创建交易品种类表。

EA 交易开发

在前一篇文章"MQL5 Cookbook: 多币种EA交易 - 简介而快速的途径"中使用的多币种EA交易将会做为模板,让我们首先决定输入参数。如上所述,我们将只使用一组参数,以下是EA交易的输入参数列表:

//--- EA交易的输入参数
sinput long                      MagicNumber          =777;      // 幻数
sinput int                       Deviation            =10;       // 滑点
sinput string                    delimeter_00=""; // --------------------------------
sinput int                       SymbolNumber         =1;        // 测试交易品种的数量
sinput bool                      RewriteParameters    =false;    // 是否重写参数
sinput ENUM_INPUTS_READING_MODE  ParametersReadingMode=FILE;     // 参数读取模式
sinput string                    delimeter_01=""; // --------------------------------
input  int                       IndicatorPeriod      =5;        // 指标周期数
input  double                    TakeProfit           =100;      // 获利
input  double                    StopLoss             =50;       // 止损
input  double                    TrailingStop         =10;       // 跟踪止损
input  bool                      Reverse              =true;     // 仓位反转
input  double                    Lot                  =0.1;      // 手数
input  double                    VolumeIncrease       =0.1;      // 仓位交易量增加量
input  double                    VolumeIncreaseStep   =10;       // 交易量增加步长

只有带有input修饰符的参数会被写入文件。下一步,我们展开以前没有遇到过的三个新参数。

  • SymbolNumber - 这个参数指出包含交易品种列表文件中的交易品种数量,如果它被设为0,列表中全部的交易品种都会被测试,如果超过了列表大小,将不会进行测试。
  • RewriteParameters - 如果此参数值设为 true,会使用当前参数值重写到指定交易品种(SymbolNumber参数指定的编号)的文件中,相反,如果它被设为false,参数会从文件中读取。
  • ParametersReadingMode - 这个参数指定输入参数的读取模式。此参数的类型是 ENUM_INPUTS_READING_MODE 自定义枚举,它提供两个选项:从文件中读取和使用当前参数。

ENUM_INPUTS_READING_MODE 枚举的代码:

//+------------------------------------------------------------------+
//| 输入参数的读取模式                                                  |
//+------------------------------------------------------------------+
enum ENUM_INPUTS_READING_MODE
  {
   FILE             = 0, // 文件
   INPUT_PARAMETERS = 1  // 输入参数
  };

交易品种的数量由之前定义的NUMBER_OF_SYMBOLS常数决定,它的值现在依赖于不同的模式,所以我们将在SYMBOLS_COUNT全局变量中修改它:

//--- 交易的交易品种数量. 它是根据测试模式和文件中交易品种的数量来计算的。
int SYMBOLS_COUNT=0;

在EA交易代码的开始部分,我们声明另外一个常数,TESTED_PARAMETERS_COUNT,它决定数组的大小和迭代输入参数时循环的次数:

//--- 测试/优化参数的数量
#define TESTED_PARAMETERS_COUNT 8

交易品种列表的文件,以及包含每个交易品种参数的文件夹都必须被放置在终端的通用文件夹中,因为它在程序的正常运行模式和测试中都能够访问。

现在我们要准备将来要用到的数组。来自前文的多币种EA交易使用的全部数组都是静态的,也就是容纳元素的大小是固定的,我们将要改变它们,我们要把它们都改成动态的,因为它们的大小现在依赖于文件中使用的交易品种数量以及输入参数的读取模式。我们还将增加新的数组(黄色高亮),在处理文件中需要它们:

//--- 输入参数的数组
string InputSymbols[];            // 交易品种名称
//---
int    InputIndicatorPeriod[];    // 指标周期数
double InputTakeProfit[];         // 获利值
double InputStopLoss[];           // 止损值
double InputTrailingStop[];       // 跟踪止损值
bool   InputReverse[];            // 仓位反转标志
double InputLot[];                // 手数值
double InputVolumeIncrease[];     // 仓位交易量增加值
double InputVolumeIncreaseStep[]; // 交易量增加步长
//--- 指标代理句柄的数组
int spy_indicator_handles[];
//--- 信号指标句柄的数组
int signal_indicator_handles[];
//--- 用于检查交易条件的数据数组
struct PriceData
  {
   double            value[];
  };
PriceData open[];      // 柱的开盘价
PriceData high[];      // 柱的最高价
PriceData low[];       // 柱的最低价
PriceData close[];     // 柱的收盘价
PriceData indicator[]; // 指标值数组
//--- 用于取得当前柱开启时间的数组
struct Datetime
  {
   datetime          time[];
  };
Datetime lastbar_time[];
//--- 用于检查每个交易品种新柱的数组
datetime new_bar[];
//--- 用于写入文件的输入参数名称数组
string input_parameters[TESTED_PARAMETERS_COUNT]=
  {
   "IndicatorPeriod",   // 指标周期数
   "TakeProfit",        // 获利
   "StopLoss",          // 止损
   "TrailingStop",      // 跟踪止损
   "Reverse",           // 仓位反转
   "Lot",               // 手数
   "VolumeIncrease",    // 仓位交易量增加量
   "VolumeIncreaseStep" // 交易量增加步长
  };
//--- 未测试的交易品种数组
string temporary_symbols[];
//--- 用于保存来自文件中的输入参数的数组,其中包含选择用来测试和交易的交易品种
double tested_parameters_from_file[];
//--- 用于写入文件的输入参数值的数组
double tested_parameters_values[TESTED_PARAMETERS_COUNT];

然后我们需要确定数组大小并将其初始化。

以上创建的输入参数值的数组将在InitializeTestedParametersValues()函数中初始化,我们将在InitializeArrays.mqh文件中创建它。该数组将用于把输入参数值写入文件。

//+------------------------------------------------------------------+
//| 初始化测试的输入参数的数组                                           |
//+------------------------------------------------------------------+
void InitializeTestedParametersValues()
  {
   tested_parameters_values[0]=IndicatorPeriod;
   tested_parameters_values[1]=TakeProfit;
   tested_parameters_values[2]=StopLoss;
   tested_parameters_values[3]=TrailingStop;
   tested_parameters_values[4]=Reverse;
   tested_parameters_values[5]=Lot;
   tested_parameters_values[6]=VolumeIncrease;
   tested_parameters_values[7]=VolumeIncreaseStep;
  }

现在让我们考虑文件操作。首先,创建另一个函数库,FileFunctions.mqh,放置到您EA交易的 UnlimitedParametersEA\Include 文件夹中。此函数库将用于创建有关文件数据读写的函数,在EA交易的主文件和项目中的其他文件中包含它。

//---包含自定义库
#include "Include\Enums.mqh"
#include "Include\InitializeArrays.mqh"
#include "Include\Errors.mqh"
#include "Include\FileFunctions.mqh"
#include "Include\TradeSignals.mqh"
#include "Include\TradeFunctions.mqh"
#include "Include\ToString.mqh"
#include "Include\Auxiliary.mqh"

一开始,我们创建用于从文本文件中读取交易品种列表的函数-ReadSymbolsFromFile()。这个文件(让我们用TestedSymbols.txt来命名它)将放置在MetaTrader5客户终端通用文件夹的\Files子文件夹下。在我的例子中,它是C:\ProgramData\MetaQuotes\Terminal\Common,但是您应该使用标准的TERMINAL_COMMONDATA_PATH常数仔细检查此路径:

TerminalInfoString(TERMINAL_COMMONDATA_PATH)
所有这些自定义文件应该被放在客户终端通用文件夹下的\Files子文件夹或者在<Terminal Data Folder>\MQL5\Files\之下,文件函数只能访问这些文件夹。

为了测试其功能,在创建的文本文件中增加几个交易品种,每个交易品种间使用回车换行符号("\r\n")分隔:

图 1. 终端通用文件夹中文件的交易品种列表

图 1. 终端通用文件夹中文件的交易品种列表

下一步,让我们看一下ReadSymbolsFromFile()函数的代码:

//+------------------------------------------------------------------+
//| 返回文件中字符串的数量 (交易品种)和                                   |
//| 填充交易品种的临时数组 temporary_symbols[]                           |
//+------------------------------------------------------------------+
//--- 当准备文件的时候,列表中的交易品种应该用行分开
int ReadSymbolsFromFile(string file_name)
  {
   int strings_count=0; // 字符串计数器
//--- 从终端通用文件夹以读方式打开文件
   int file_handle=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_COMMON);
//--- 如果文件句柄已经得到
   if(file_handle!=INVALID_HANDLE)
     {
      ulong  offset =0; // 文件指针位置的偏移
      string text ="";  // 读取的字符串将会写入此变量
      //--- 读取文件直到当前文件指针到达文件末尾或者程序退出
      while(!FileIsEnding(file_handle) || !IsStopped())
        {
         //--- 读取文件直到当前文件指针到达文件末尾或者程序退出
         while(!FileIsLineEnding(file_handle) || !IsStopped())
           {
            //--- 读取整个字符串
            text=FileReadString(file_handle);
            //--- 取得指针位置
            offset=FileTell(file_handle);
            //--- 如果还没有到文件末尾,读取下一个字符串
            //    为了这个目的,增加文件指针的偏移
            if(!FileIsEnding(file_handle))
               offset++;
            //--- 移动到下一个字符串
            FileSeek(file_handle,offset,SEEK_SET);
            //--- 如果字符串不为空
            if(text!="")
              {
               //--- 增加字符串计数器
               strings_count++;
               //--- 增加字符串数组大小,
               ArrayResize(temporary_symbols,strings_count);
               //--- 把读取的字符串写到当前索引所指位置
               temporary_symbols[strings_count-1]=text;
              }
            //--- 退出嵌套的循环
            break;
           }
         //--- 如果这是文件末尾, 结束主循环
         if(FileIsEnding(file_handle))
            break;
        }
      //--- 关闭文件
      FileClose(file_handle);
     }
//--- 返回文件字符串数量
   return(strings_count);
  }

这样,文本文件就依行读出了,读到的金融资产名称将写到之前创建的temporary_symbols[] 临时数组中 (交易品种在交易服务器上是否能够访问将晚点检查)。 而且,在函数的末尾返回读取字符串的数量,也就是将要在EA交易中测试的交易品种数量。

对于以前没有遇到的函数和标识符,请一定在MQL5 参考中查阅详细信息。代码中提供的注释会使学习简单很多。

让我们回到InitializeArrays.mqh文件,在那里我们将要创建InitializeInputSymbols()函数,用于填充之前声明的InputSymbols[] 交易品种名称数组。初学者可能觉得它非常复杂,所以我在代码中提供了详细注释:

//+------------------------------------------------------------------+
//| 填充交易品种的InputSymbol[] 数组                                    |
//+------------------------------------------------------------------+
void InitializeInputSymbols()
  {
   int    strings_count=0;    // 交易品种文件的字符串数量
   string checked_symbol="";  // 检查交易品种在交易服务器上是否可以访问
//--- 从"TestedSymbols.txt" 文件中取得交易品种数量
   strings_count=ReadSymbolsFromFile("TestedSymbols.txt");
//--- 如果在优化模式下或者两种模式之一(测试或可视化测试), 使用指定的交易品种数量
   if(IsOptimization() || ((IsTester() || IsVisualMode()) && SymbolNumber>0))
     {
      //--- 决定参数优化中的交易品种
      for(int s=0; s<strings_count; s++)
        {
         //--- 如果在参数中指定的数量和当前循环的索引相同
         if(s==SymbolNumber-1)
           {
            //--- 检查交易品种是否在交易服务器上
            if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="")
              {
               //--- 设置交易品种数量
               SYMBOLS_COUNT=1;
               //--- 设置交易品种数组大小
               ArrayResize(InputSymbols,SYMBOLS_COUNT);
               //--- 写下交易品种名称
               InputSymbols[0]=checked_symbol;
              }
            //--- 退出
            return;
           }
        }
     }
//--- 在测试或者可视化测试模式下,如果您需要测试文件列表里的全部交易品种
   if((IsTester() || IsVisualMode()) && SymbolNumber==0)
     {
      //--- 参数读取方式: 从文件读取
      if(ParametersReadingMode==FILE)
        {
         //--- 迭代文件中的所有交易品种
         for(int s=0; s<strings_count; s++)
           {
            //--- 检查交易品种是否在交易服务器上
            if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="")
              {
               //--- 增加交易品种计数器
               SYMBOLS_COUNT++;
               //--- 设置交易品种数组大小
               ArrayResize(InputSymbols,SYMBOLS_COUNT);
               //--- 写下交易品种名称
               InputSymbols[SYMBOLS_COUNT-1]=checked_symbol;
              }
           }
         //--- 退出
         return;
        }
      //--- 参数读取模式: 从EA交易的输入参数中读取
      if(ParametersReadingMode==INPUT_PARAMETERS)
        {
         //--- 设置交易品种数量
         SYMBOLS_COUNT=1;
         //--- 设置交易品种数组大小
         ArrayResize(InputSymbols,SYMBOLS_COUNT);
         //--- 写下当前交易品种名称
         InputSymbols[0]=Symbol();
         //--- 退出
         return;
        }
     }
//--- 在EA交易的正常运行模式下,使用当前图表的交易品种
   if(IsRealtime())
     {
      //--- 设置交易品种数量
      SYMBOLS_COUNT=1;
      //--- 设置交易品种数组大小
      ArrayResize(InputSymbols,SYMBOLS_COUNT);
      //--- 写下交易品种名称
      InputSymbols[0]=Symbol();
     }
//---
  }

当根据使用交易品种的数量确定输入参数数组的大小以后,您需要设置其他输入参数的数组。让我们在一个独立函数 - ResizeInputParametersArrays()中实现它:

//+------------------------------------------------------------------+
//| 设置输入参数数组的新的大小                                           |
//+------------------------------------------------------------------+
void ResizeInputParametersArrays()
  {
   ArrayResize(InputIndicatorPeriod,SYMBOLS_COUNT);
   ArrayResize(InputTakeProfit,SYMBOLS_COUNT);
   ArrayResize(InputStopLoss,SYMBOLS_COUNT);
   ArrayResize(InputTrailingStop,SYMBOLS_COUNT);
   ArrayResize(InputReverse,SYMBOLS_COUNT);
   ArrayResize(InputLot,SYMBOLS_COUNT);
   ArrayResize(InputVolumeIncrease,SYMBOLS_COUNT);
   ArrayResize(InputVolumeIncreaseStep,SYMBOLS_COUNT);
  }

现在,我们需要创建一种机制允许我们为每个交易品种读取输入参数的值以及根据所选的输入模式和EA交易的设置把那些参数值写到一个单独的文件(为每个交易品种)中,这和我们从文件中读取交易品种列表非常相似。它是一个复杂的任务,所以我们把它分解为几个过程。

首先,我们需要从文件中读取输入参数再放到数组中,数组将包含双精度类型的数值。晚些时候我们会把它们中的一些(指标周期和仓位反转)转换为对应的整数型和布林类型。打开文件的句柄以及存储来自文件的参数值的数组会传给ReadInputParametersValuesFromFile()函数:

//+----------------------------------------------------------------------+
//| 从文件中读取参数并且在传入的数组中保存它们                                 |
//| 文本文件格式: key=value                                                |
//+----------------------------------------------------------------------+
bool ReadInputParametersValuesFromFile(int handle,double &array[])
  {
   int    delimiter_position  =0;  // 字符串中 "=" 字符的位置
   int    strings_count       =0;  // 字符串计数器
   string read_string         =""; // 读取的字符串
   ulong  offset              =0;  // 指针的位置 (偏移字节数)
//--- 把文件指针移动到文件开始位置
   FileSeek(handle,0,SEEK_SET);
//--- 读取文件直到当前文件指针位置到达文件末尾
   while(!FileIsEnding(handle))
     {
      //--- 如果用户移除了程序
      if(IsStopped())
         return(false);
      //--- 读取直到字符串末尾
      while(!FileIsLineEnding(handle))
        {
         //--- 如果用户移除了程序
         if(IsStopped())
            return(false);
         //--- 读取字符串
         read_string=FileReadString(handle);
         //--- 取得所读取字符串中隔离符号("=")的索引
         delimiter_position=StringFind(read_string,"=",0);
         //--- 取得隔离符号 ("=") 后直到字符串末尾的内容
         read_string=StringSubstr(read_string,delimiter_position+1);
         //--- 把取得的值转换为双精度类型放到输出数组中
         array[strings_count]=StringToDouble(read_string);
         //--- 取得文件指针的当前位置
         offset=FileTell(handle);
         //--- 如果是字符串末尾
         if(FileIsLineEnding(handle))
           {
            //--- 如果不是文件末尾移动到下一个字符串
            if(!FileIsEnding(handle))
               //--- 把文件指针偏移增加1以移动到下一个字符串
               offset++;
            //--- 移动文件指针至文件开头的偏移量
            FileSeek(handle,offset,SEEK_SET);
            //--- 增加字符串计数器
            strings_count++;
            //--- 退出嵌套的读取字符串的循环
            break;
           }
        }
      //--- 如果是文件末尾,退出主循环
      if(FileIsEnding(handle))
         break;
     }
//--- 返回成功结束的事实
   return(true);
  }

我们将会给这个函数传入怎样的文件句柄和数组呢?这依赖于我们从"TestedSymbols.txt"文件中读取到那些交易品种,每个交易品种都对应着某个包含着输入参数值的文本文件。有两种情形需要考虑:

  1. 文件存在并且我们使用以上的ReadInputParametersValuesFromFile()函数读出了输入参数的值。
  2. 文件不存在或者我们需要重写已经存在的输入参数值。

包含输入参数的文本文件的格式(一个.ini文件,尽管您可以选择任何您觉得有必要的其他扩展名) 将很简单:

input_parameter_name1=value
input_parameter_name2=value
....
input_parameter_nameN=value

让我们把逻辑结合到一个单独的函数中,ReadWriteInputParameters(), 其代码如下:

//+------------------------------------------------------------------+
//| 为一个交易品种从文件读取输入参数或写输入参数到文件中                    |
//+------------------------------------------------------------------+
void ReadWriteInputParameters(int symbol_number,string path)
  {
   string file_name=path+InputSymbols[symbol_number]+".ini"; // 文件名
//---
   Print("查找文件 '"+file_name+"' ...");
//--- 打开包含交易品种输入参数的文件
   int file_handle_read=FileOpen(file_name,FILE_READ|FILE_ANSI|FILE_COMMON);
   
//--- 情形 #1: 文件存在且参数值不需要重写
   if(file_handle_read!=INVALID_HANDLE && !RewriteParameters)
     {
      Print("文件 '"+InputSymbols[symbol_number]+".ini' 存在,开始读取...");
      //--- 设置数组大小
      ArrayResize(tested_parameters_from_file,TESTED_PARAMETERS_COUNT);
      //--- 使用从文件中读取的值填充数组
      ReadInputParametersValuesFromFile(file_handle_read,tested_parameters_from_file);
      //--- 如果数组大小是正确的
      if(ArraySize(tested_parameters_from_file)==TESTED_PARAMETERS_COUNT)
        {
         //--- 把参数值写到数组中
         InputIndicatorPeriod[symbol_number]    =(int)tested_parameters_from_file[0];
         Print("InputIndicatorPeriod[symbol_number] = "+(string)InputIndicatorPeriod[symbol_number]);
         InputTakeProfit[symbol_number]         =tested_parameters_from_file[1];
         InputStopLoss[symbol_number]           =tested_parameters_from_file[2];
         InputTrailingStop[symbol_number]       =tested_parameters_from_file[3];
         InputReverse[symbol_number]            =(bool)tested_parameters_from_file[4];
         InputLot[symbol_number]                =tested_parameters_from_file[5];
         InputVolumeIncrease[symbol_number]     =tested_parameters_from_file[6];
         InputVolumeIncreaseStep[symbol_number] =tested_parameters_from_file[7];
        }
      //--- 关闭文件并退出
      FileClose(file_handle_read);
      return;
     }
//--- 情形 #2: 如果文件不存在或者参数需要被重写
   if(file_handle_read==INVALID_HANDLE || RewriteParameters)
     {
      //--- 关闭读取文件句柄
      FileClose(file_handle_read);
      //--- 获得写入文件句柄
      int file_handle_write=FileOpen(file_name,FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,"");
      //--- 如果已经获得了句柄
      if(file_handle_write!=INVALID_HANDLE)
        {
         string delimiter="="; // 分隔符号
         //--- 写入参数
         for(int i=0; i<TESTED_PARAMETERS_COUNT; i++)
           {
            FileWrite(file_handle_write,input_parameters_names[i],delimiter,tested_parameters_values[i]);
            Print(input_parameters_names[i],delimiter,tested_parameters_values[i]);
           }
         //--- 把参数值写到数组中
         InputIndicatorPeriod[symbol_number]    =(int)tested_parameters_values[0];
         InputTakeProfit[symbol_number]         =tested_parameters_values[1];
         InputStopLoss[symbol_number]           =tested_parameters_values[2];
         InputTrailingStop[symbol_number]       =tested_parameters_values[3];
         InputReverse[symbol_number]            =(bool)tested_parameters_values[4];
         InputLot[symbol_number]                =tested_parameters_values[5];
         InputVolumeIncrease[symbol_number]     =tested_parameters_values[6];
         InputVolumeIncreaseStep[symbol_number] =tested_parameters_values[7];
         //--- 根据指向打印相关信息
         if(RewriteParameters)
            Print("文件 '"+InputSymbols[symbol_number]+".ini' 参数 '"+EXPERT_NAME+".ex5 EA交易已经被重写'");
         else
            Print("文件 '"+InputSymbols[symbol_number]+".ini' 参数 '"+EXPERT_NAME+".ex5 EA交易已经被创建'");
        }
      //--- 关闭写入文件句柄
      FileClose(file_handle_write);
     }
  }

最后的文件函数CreateInputParametersFolder()会在客户终端的通用文件夹下使用EA交易的名称创建一个文件夹,含有输入参数值的文本文件(我们例子中的 .ini 文件)就在此文件夹下读/写。就像前一个函数一样,我们将检查文件夹是否存在,如果文件夹已经被成功创建或者已经存在,此函数会返回文件夹路径,如果出错则返回空字符串:

//+----------------------------------------------------------------------------------+
//| 如果包含输入参数的文件夹不存在则创建文件夹                                             |
//| 如果成功则返回文件夹路径                                                             |
//+----------------------------------------------------------------------------------+
string CreateInputParametersFolder()
  {
   long   search_handle       =INVALID_HANDLE;   // 搜索文件夹/文件的句柄
   string EA_root_folder      =EXPERT_NAME+"\\"; // EA交易的根文件夹
   string returned_filename   ="";               // 找到的对象的名称(文件/文件夹)
   string search_path         ="";               // 搜索路径
   string folder_filter       ="*";              // 搜索过滤条件 (* - 检查所有文件/文件夹)
   bool   is_root_folder      =false;            // EA交易根目录是否存在的标志

//--- 查找EA交易的根文件夹
   search_path=folder_filter;
//--- 设置搜索句柄在终端的通用目录中
   search_handle=FileFindFirst(search_path,returned_filename,FILE_COMMON);
//--- 如果第一个目录不是根目录,做标记
   if(returned_filename==EA_root_folder)
      is_root_folder=true;
//--- 如果获得了搜索句柄
   if(search_handle!=INVALID_HANDLE)
     {
      //--- 如果第一个目录不是根目录
      if(!is_root_folder)
        {
         //--- 迭代所有文件以查找根目录
         while(FileFindNext(search_handle,returned_filename))
           {
            //--- 用户终止了搜索过程
            if(IsStopped())
               return("");
            //--- 如果找到了,标记它
            if(returned_filename==EA_root_folder)
              {
               is_root_folder=true;
               break;
              }
           }
        }
      //--- 关闭根目录搜索句柄
      FileFindClose(search_handle);
      //search_handle=INVALID_HANDLE;
     }
//--- 否则打印错误信息
   else
      Print("获取搜索句柄出错或者"
            "目录 '"+TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"' 为空: ",ErrorDescription(GetLastError()));

//--- 根据检查结果,创建必要的文件夹
   search_path=EXPERT_NAME+"\\";
//--- 如果EA交易的根文件夹不存在
   if(!is_root_folder)
     {
      //--- 创建它
      if(FolderCreate(EXPERT_NAME,FILE_COMMON))
        {
         //--- 如果文件夹已经被创建,标记它
         is_root_folder=true;
         Print("根目录 '..\\"+EXPERT_NAME+"\\ EA交易已经被创建'");
        }
      else
        {
         Print("创建 "
               "EA交易的根文件夹出错: ",ErrorDescription(GetLastError()));
         return("");
        }
     }
//--- 如果所需文件夹已经存在
   if(is_root_folder)
      //--- 返回创建文件的路径以为EA交易写入参数
      return(search_path+"\\");
//--- 如有错误,返回空字符串
   return("");
  }

让我们现在把以上函数都放在一个单独的函数 - InitializeInputParametersArrays() 中调用。在EA交易初始化过程中,此函数使用4个输入参数:

  1. 标准操作模式(或者为所选交易品种做参数优化)为使用当前的输入参数值;
  2. 在测试和优化过程中向文件中重写参数;
  3. 测试所选的交易品种;
  4. 测试文件中列出的全部交易品种。

所有的操作在代码中都有详细的注释做解释:

//+-------------------------------------------------------------------+
//| 根据模式初始化输入参数数组                                            |
//+-------------------------------------------------------------------+
void InitializeInputParametersArrays()
  {
   string path=""; // 包含输入参数文件的文件夹
//--- 模式 #1 :
//    - EA交易的标准运行模式或者
//    - 优化模式或者
//    - 从EA交易中读取输入参数而不重写到文件
   if(IsRealtime() || IsOptimization() || (ParametersReadingMode==INPUT_PARAMETERS && !RewriteParameters))
     {
      //--- 使用当前值初始化参数数组
      InitializeWithCurrentValues();
      return;
     }
//--- 模式 #2 :
//    - 为指定交易品种在文件中重写参数
   if(RewriteParameters)
     {
      //--- 使用当前值初始化参数数组
      InitializeWithCurrentValues();
      //--- 如果EA交易的文件夹已经存在或者成功创建了
      if((path=CreateInputParametersFolder())!="")
         //--- 读/写交易品种参数的文件
         ReadWriteInputParameters(0,path);
      //---
      return;
     }
//--- 模式 #3 :
//    - 为所选的交易品种测试 (可以是可视化测试,非优化状态) EA交易
   if((IsTester() || IsVisualMode()) && !IsOptimization() && SymbolNumber>0)
     {
      //--- 如果EA交易的文件夹已经存在或者成功创建了
      if((path=CreateInputParametersFolder())!="")
        {
         //--- 迭代所有交易品种 (在这种情况下, 交易品种的数目 = 1)
         for(int s=0; s<SYMBOLS_COUNT; s++)
            //--- 读写交易品种参数文件
            ReadWriteInputParameters(s,path);
        }
      return;
     }
//--- 模式 #4 :
//    - 为所有交易品种测试 (可以是可视化测试,非优化状态) EA交易
   if((IsTester() || IsVisualMode()) && !IsOptimization() && SymbolNumber==0)
     {
      //--- 如果EA交易的文件夹存在并且
      //    在创建过程中没有错误
      if((path=CreateInputParametersFolder())!="")
        {
         //--- 迭代所有交易品种
         for(int s=0; s<SYMBOLS_COUNT; s++)
            //--- 读写交易品种参数文件
            ReadWriteInputParameters(s,path);
        }
      return;
     }
  }

在模式 #1 和 #2 情况下,我们使用 InitializeWithCurrentValues() 函数。它会初始化0 (单独) 索引的当前输入参数值。换句话说,这个函数在仅需要一个交易品种时使用:

//+------------------------------------------------------------------+
//| 初始化输入参数数组值为当前值                                         |
//+------------------------------------------------------------------+
void InitializeWithCurrentValues()
  {
   InputIndicatorPeriod[0]=IndicatorPeriod;
   InputTakeProfit[0]=TakeProfit;
   InputStopLoss[0]=StopLoss;
   InputTrailingStop[0]=TrailingStop;
   InputReverse[0]=Reverse;
   InputLot[0]=Lot;
   InputVolumeIncrease[0]=VolumeIncrease;
   InputVolumeIncreaseStep[0]=VolumeIncreaseStep;
  }

现在我们只需要做最简单的部分了,然而也是最重要的。在程序入口点,即OnInit()函数中依次调用以上的函数:

//+------------------------------------------------------------------+
//| EA初始化函数                                                       |
//+------------------------------------------------------------------+
void OnInit()
  {
//--- 初始化测试过的输入参数数组用于写入文件
   InitializeTestedParametersValues();
//--- 填充交易品种名称数组
   InitializeInputSymbols();
//--- 设置输入参数数组大小
   ResizeInputParametersArrays();
//--- 初始化指标句柄数组
   InitializeIndicatorHandlesArrays();
//--- 根据EA交易的运行模式初始化输入参数数组
   InitializeInputParametersArrays();
//--- 从 "EventsSpy.ex5" 指标获取代理句柄
   GetSpyHandles();
//--- 取得指标句柄
   GetIndicatorHandles();
//--- 初始化新柱
   InitializeNewBarArray();
//--- 初始化价格数组合指标缓冲区
   ResizeDataArrays();
  }

这样,我们的代码就写完了。您可以从文章附件中的源代码文件中自学以上描述的函数,它们没有什么复杂的。现在让我们进一步看看结果以及怎样使用它。

优化参数和测试EA交易

我们已经说过,您应该把交易品种列表的TestedSymbols.txt文件放置到客户终端的通用文件夹下。为了举例/测试的目的,我们会创建一个三个交易品种的列表: AUDUSDEURUSD 和 NZDUSD。现在我们将依次为每个交易品种单独优化输入参数,策略测试器需要如下设置:

图 2. 策略测试器设置.

图 2. 策略测试器设置.

您可以在“设置”页面设置任何交易品种(我们的例子中是EURUSD) ,因为它不影响EA交易的工作。然后,我们设置EA交易优化的参数:

图 3. EA交易的输入参数.

图 3. EA交易的输入参数.

上图显示SymbolNumber参数 (测试交易品种的数量)被设为 1。它的意思是,当运行优化时,EA交易将使用TestedSymbols.txt文件中交易品种列表的第一个交易品种,在我们的实例中,也就是AUDUSD

注意: 因为此EA交易的特殊性 (根据读取文本文件设置交易品种列表), 无法通过远程代理进行优化。

我们将在本系列以后的文章中尝试解决这个限制。

在完成优化以后,您可以运行测试,研究不同优化通过的结果差异。如果您想要EA交易从文件中读取参数,您应该在ParametersReadingMode参数(参数读取模式)的下拉列表中选择文件 ,为了能够使用EA交易的当前参数(在“设定”页面设置), 您则应该选择输入参数选项。

当然,在查看优化结果的时候也需要输入参数选项。当第一次运行测试时,EA交易将在终端通用文件夹下创建对应名称的文件夹,所创建的文件夹将包含测试交易品种的当前参数。在我们的实例中,它是AUDUSD.ini. 您可以在下图中看到此文件的内容:

图 4. 交易品种文件中的输入参数列表.

图 4. 交易品种文件中的输入参数列表.

当找到所需参数的组合时,您需要把RewriteParameters参数(重写参数)设为true并且再次运行测试。参数文件将被更新。之后您可以再次把参数设为false并且测试优化通过的结果,如果需要比较写入文件的值以及输入参数中设定的值,切换参数读取模式参数的选项也非常方便。

然后,我们为EURUSD运行优化,它是交易品种列表文件的第二个交易品种。为了这样做,我们需要把测试交易品种编号参数设为2。 优化以后,确定好了参数并把他们写入文件后,针对列表中的第三个交易品种也要同样去做。

当所有交易品种的参数都写入文件以后,您可以通过指定交易品种编号独立查看每个交易品种的结果,也可以把测试交易品种编号设为0来查看所有交易品种的累积结果。以下我已经得到了全部交易品种的累积结果:

图 5. 多币种EA交易的累积结果.

图 5. 多币种EA交易的累积结果.

结论

最终,我们得到了一个针对多币种EA交易相对方便的开发模板,如果需要,它可以被进一步开发。文章的附件中是可以下载的EA交易的文件存档,在解压缩之后,把UnlimitedParametersEA 文件夹放到<MetaTrader 5终端文件夹>\MQL5\Experts之下。EventsSpy.mq5 指标应该放在<MetaTrader 5终端文件夹>\MQL5\Indicators之下。 另外,别忘了在客户终端的通用文件夹下创建TestedSymbols.txt文本文件。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/650

附加的文件 |
eventsspy__2.mq5 (13.33 KB)
6 步创建您自己的交易机器人! 6 步创建您自己的交易机器人!

如果您不清楚交易类如何构造,而且一看到面向对象编程之类的词就害怕,那么,本文正适合您。实际上,那些编写您自己的交易信号模块的细节,您无需知道。只需遵循几条简单法则即可。MQL5 向导会完成所有其余工作,而您则会得到一个即用型的交易机器人!

面向对象法建立多时间表及多货币面板 面向对象法建立多时间表及多货币面板

本文讲述如何利用面向对象编程创建 MetaTrader 5 多时间表与多货币面板。主要目标在于建立一个可用于显示多种不同类型数据(比如价格、价格变动、指标值或自定义买/卖条件)、且无需修改面板本身代码的通用面板。

MQL5 Cookbook: 把交易历史写入文件以及为每个交易品种在Excel中创建余额图表 MQL5 Cookbook: 把交易历史写入文件以及为每个交易品种在Excel中创建余额图表

当在各种论坛做沟通时,我经常使用我自己的测试结果作为例子,这些结果显示为Microsoft Excel中的图表截图。很多时候都有人问我这些图表是怎样创建的,最终,我现在有时间在本文中解释其中的全部了。

MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失 MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失

无论您使用何种交易策略,总会有一个问题:怎样选择参数以保证未来的利润。本文提供了一个EA交易的实例,使您可以同时优化多个交易品种的参数,这种方法是未了减少参数的过度配合以及处理在研究中来自单个交易品种的数据不足的问题。