关于策略优化的一些简单想法

Jose Miguel Soriano | 3 八月, 2015

简介

当发现一个稳定的EA策略,我们往往是将它加载在EURUSD图上进行交易的,不是吗?那么这个策略能在其他货币对上获利更多吗?此策略是否能在无需几何倍增交易量的前提下,在其他货币对上能有更佳的表现呢?

如果我们对于EURUSD 1H 下的运行结果不满意要怎么办呢?换到EURJPY H4 ?

另外,即使我们有一个64位的操作系统,让我们不必担心测试速度,难道我们忘了那些可怕的交易系统输入参数组合吗。所有的枚举组合在优化测试时都会被执行,而很多结果我们在最终的测试报告中又是不得不忽略的。

我已经解决了这些“小问题”,并且在本文中和大家分享这些有效的解决办法。如果有其他更好的解决方案也请告诉我。


优化时间框架

MQL5提供了一组时间框架:从M1, M2 , M3, M4,... H1, H2,... 一直到月图。总共,有21种时间框架。在优化测试过程中,我们想要知道哪个时间框架下我们的策略最有效—是如M1和M5这类短时间周期,还是中等时间周期如H2和H4,亦或是长时间周期如D1和W1。

其实我们并不需要这么多选择。任何情况下,如果我们发现策略在M5时间周期下被证明是有效的,那么下一步我们可以在M3或M6上进行优化,检验策略的表现。

如果我们使用ENUM_TIMEFRAMES类型的输入参数:

input ENUM_TIMEFRAMES marcoTF= PERIOD_M5; 

那么优化器将能提供21种参数。我们真的需要这么多参数吗?

时间周期的标准选项

我们其实并不需要。那要怎么简化优化过程呢?首先我们可以定义枚举值:

enum mis_MarcosTMP
{
   _M1= PERIOD_M1,
   _M5= PERIOD_M5,
   _M15=PERIOD_M15,
//   _M20=PERIOD_M20,
   _M30=PERIOD_M30,
   _H1= PERIOD_H1,
   _H2= PERIOD_H2,
   _H4= PERIOD_H4,
//   _H8= PERIOD_H8,
   _D1= PERIOD_D1,
   _W1= PERIOD_W1,
   _MN1=PERIOD_MN1
};

我们可以添加或者删除不需要的时间周期。在代码的开头部分定义输入参数用于优化测试:

input mis_MarcosTMP timeframe= _H1;

在library .mqh中定义一个新的函数:

//----------------------------------------- DEFINE THE TIMEFRAME ----------------------------------------------------------
ENUM_TIMEFRAMES defMarcoTiempo(mi_MARCOTMP_CORTO marco)
{
   ENUM_TIMEFRAMES resp= _Period;
   switch(marco)
   {
      case _M1: resp= PERIOD_M1; break;
      case _M5: resp= PERIOD_M5; break;
      case _M15: resp= PERIOD_M15; break;
      //case _M20: resp= PERIOD_M20; break;
      case _M30: resp= PERIOD_M30; break;
      case _H1: resp= PERIOD_H1; break;
      case _H2: resp= PERIOD_H2; break;
      case _H4: resp= PERIOD_H4; break;
      //case _H8: resp= PERIOD_H8; break;
      case _D1: resp= PERIOD_D1; break;
      case _W1: resp= PERIOD_W1; break;
      case _MN1: resp= PERIOD_MN1;
   }
return(resp);
}

声明一个新的全局变量:

ENUM_TIMEFRAMES marcoTmp= defMarcoTiempo(marcoTiempo);          //时间周期被定义为全局变量

"marcoTmp" 是一个全局变量,用于定义EA所使用的图表周期。在优化器参数列表,可以定义 "marcoTiempo" 变量的加载间隔。这就可以仅仅针对我们感兴趣的值进行测试,而无需花费时间和资源去分析M6或M12。通过这种方法我们能够在不同的时间周期下分析EA的表现。

时间周期的用户选项

当然可以这样实现

ENUM_TIMEFRAMES marcoTmp= (ENUM_TIMEFRAMES)marcoTiempo;

显然要得到上面的代码实现方式,你一定是一个致力于简化代码的完美主义者并且具备丰富的程序开发经验。或者你正在使用VPS并不断优化计算性能以节省开支。


优化一个货币对或者一组货币对

在MetaTrader 5 策略测试器中有一种优化模式,能使EA运行于市场观察窗口中所有的货币对上。然而如果选中的货币对是另一个参数的话,这个函数不能用于优化测试。因此,如果选则了15个货币对,那么测试器将执行15个测试进程。如何才能找到最适合我们EA的货币对呢?如果这是一个多货币对EA,那么哪一组货币对,怎样的参数组合能得出最好的结果呢?在MQL5中,字符串变量是不能作为优化变量的。如何才能做到呢?

用下面的方式将一个货币或者一组货币定义为输入参数:

input int selecDePar= 0;

string cadParesFX= selecPares(selecDePar);

"selecDePar" 作为优化参数,将其类型转换为字符串变量。在EA中使用"cadParesFX" 变量。货币对(在此和是否是多货币对策略无关)名称将和其他要优化的常规变量一起存储在这个变量中。

//------------------------------------- SELECT THE SET OF PAIRS -------------------------------------
string selecPares(int combina= 0)
{
   string resp="EURUSD";
   switch(combina)               
      {
         case 1: resp= "EURJPY"; break;
         case 2: resp= "USDJPY"; break;
         case 3: resp= "USDCHF"; break;      
         case 4: resp= "GBPJPY"; break;
         case 5: resp= "GBPCHF"; break;      
         case 6: resp= "GBPUSD"; break;
         case 7: resp= "USDCAD"; break;
         case 8: resp= "CADJPY"; break;      
         case 9: resp= "XAUUSD"; break;
       
         case 10: resp= "EURJPY;USDJPY"; break;
         case 11: resp= "EURJPY;GBPJPY"; break;
         case 12: resp= "GBPCHF;GBPJPY"; break;
         case 13: resp= "EURJPY;GBPCHF"; break;
         case 14: resp= "USDJPY;GBPCHF"; break;

         case 15: resp= "EURUSD;EURJPY;GBPJPY"; break;
         case 16: resp= "EURUSD;EURJPY;GBPCHF"; break;
         case 17: resp= "EURUSD;EURJPY;USDJPY"; break;
         case 18: resp= "EURJPY;GBPCHF;USDJPY"; break;
         case 19: resp= "EURJPY;GBPUSD;GBPJPY"; break;
         case 20: resp= "EURJPY;GBPCHF;GBPJPY"; break;
         case 21: resp= "USDJPY;GBPCHF;GBPJPY"; break;
         case 22: resp= "EURUSD;USDJPY;GBPJPY"; break;
       
         case 23: resp= "EURUSD;EURJPY;USDJPY;GBPUSD;USDCHF;USDCAD"; break;
         case 24: resp= "EURUSD;EURJPY;USDJPY;GBPUSD;USDCHF;USDCAD;AUDUSD"; break;
      }
   return(resp);
}

要定义何种货币对组合以及通知测试器以何种间距进行分析,这取决于我们的目标是什么。给策略测试器一个指令,让"selecDePar" 参数以步长为1从15到22进行优化测试(见下图)。如果我们想要比较单个货币对的结果要怎么办呢?这种情况下,只需要以步长为1从0到9运行优化程序。

优化一组货币对

例如,EA中参数cadParesFX= "EURUSD;EURJPY;GBPCHF"。 在OnInit()函数中调用 "cargaPares()" 函数,它将用变量cadParesFX中以分号分隔的字符串对动态数组arrayPares[]进行赋值所有全局变量都必须加载到动态数组中,用于保存每个货币的值,包括控制货币对的新增柱形。针对只在一个货币对上运行的情况,数组的长度等于1。

//-------------------------------- STRING CONVERSION FROM CURRENCY PAIRS INTO AN ARRAY  -----------------------------------------------
int cargaPares(string cadPares, string &arrayPares[])
{            //convierte "EURUSD;GBPUSD;USDJPY" a {"EURUSD", "GBPUSD", "USDJPY"}; devuelve el número de paresFX
   string caract= "";
   int i= 0, k= 0, contPares= 1, longCad= StringLen(cadPares);
   if(cadPares=="")
   {
      ArrayResize(arrayPares, contPares);
      arrayPares[0]= _Symbol;
   }
   else
   {
      for (k= 0; k<longCad; k++) if (StringSubstr(cadPares, k, 1)==";") contPares++;
      ArrayResize(arrayPares, contPares);    
      ZeroMemory(arrayPares);
      for(k=0; k<longCad; k++)
      {
         caract= StringSubstr(cadPares, k, 1);
         if (caract!=";") arrayPares[i]= arrayPares[i]+caract;
         else i++;
      }
    }
   return(contPares);
}

在OnInit() 中这个函数的实现方式如下:

string ar_ParesFX[];    //包含EA所运行的货币对名称的数组
int numSimbs= 1;        //变量,EA运行货币对的编号

int OnInit()
{
   
   //...
   numSimbs= cargaPares(cadParesFX, ar_ParesFX);     //返回ar_ParesFX 数组,EA运行的货币对
   //...
   
}

如果numSimbs>1, OnChartEvent() 函数将被调用。这对于多货币对系统有用。否则,使用OnTick()函数:

void OnTick()
{
   string simb="";
   bool entrar= (nSimbs==1);
   if(entrar)
   {   
      .../...
      simb= ar_ParesFX[0];
      gestionOrdenes(simb);
      .../...
   }
   return;
}

//+------------------------------------------------------------------+
//| EVENT HANDLER                                                   |
//+-----------------------------------------------------------------+
void OnChartEvent(const int idEvento, const long& lPeriodo, const double& dPrecio, const string &simbTick)
{
   bool entrar= nSimbs>1 && (idEvento>=CHARTEVENT_CUSTOM);
   if(entrar)      
   {
      .../...
      gestionOrdenes(simbTick);
      .../...
   }
}
  

这就意味着所有函数的参数必须至少包含货币对名称变量。例如,Digits() 函数我们必须用如下的形式代替:

//--------------------------------- SYMBOLS OF A SYMBOL ---------------------------------------
int digitosSimb(string simb= NULL)
{
   int numDig= (int)SymbolInfoInteger(simb, SYMBOL_DIGITS);
   return(numDig);
}

换句话说,我们必须忘掉Symbol()或Point()函数,以及MetaТtarder 4 中其他常规的变量如Ask和Bid。

//----------------------------------- POINT VALUE in price (Point())---------------------------------
double valorPunto(string simb= NULL) 
{
   double resp= SymbolInfoDouble(simb, SYMBOL_POINT);
   return(resp);
}
//--------------------------- precio ASK-BID  -----------------------------------------
double precioAskBid(string simb= NULL, bool ask= true)
{
   ENUM_SYMBOL_INFO_DOUBLE precioSolic= ask? SYMBOL_ASK: SYMBOL_BID;
   double precio= SymbolInfoDouble(simb, precioSolic);
   return(precio);
}

控制新柱线到来的函数也得修正。如果预示EURUSD新柱线开盘的报价到来,而USDJPY可能在未来的2秒内都没有新的报价,当接下来USDJPY的报价到来并通知EA这是新柱线的开盘时,其实EURUSD在2秒前就已经产生了新柱形。

//------------------------------------- NEW MULTI-CURRENCY CANDLESTICK -------------------------------------
bool nuevaVelaMD(string simb= NULL, int numSimbs= 1, ENUM_TIMEFRAMES marcoTmp= PERIOD_CURRENT)
{
        static datetime arrayHoraNV[];
        static bool primVez= true;
        datetime horaVela= iTime(simb, marcoTmp, 0);    //当前蜡烛线的开始时间
        bool esNueva= false;
        int codS= buscaCadArray(simb, nombreParesFX);      
        if(primVez)
        {
           ArrayResize(arrayHoraNV, numSimbs);
           ArrayInitialize(arrayHoraNV, 0);     
           primVez= false;
        }
        esNueva= codS>=0? arrayHoraNV[codS]!= horaVela: false;
        if(esNueva) arrayHoraNV[codS]= horaVela;
        return(esNueva); 
}

这个方法让我能够在一次优化期间就得出:

在M5时间周期及其他一些优化参数组合下真的能有很好的结果,但是在H1或H2时间周期下却不然。

这就有点令人费解了。关于此我请求过技术支持。我不知道为何会这样,但策略测试器中优化测试的结果随选择的货币对不同而不同。这也是我在策略开发中始终使用此货币对来检验测试结果的原因,确保这是优化器能够进行分析的货币对之一。


参数组合的优化

有时候,在优化过程中一些看似不合逻辑的参数组合却能取得较好的结果。有时候这样策略看起来不合理。例如:"maxSpread"定义入场点差,我们在很多货币对上对它进行优化测试,这些经纪商的平均点差一般小于30,XAUUSD小于400。如果货币对点差超过50,XAUUSD小于200,那么这样的分析就没有意义。将数据传送到优化器,设置“maxSpread以20的间距从0到600”,但是这样和其他参数一起组成无数种组合的设置是没有意义的。

根据前面描述的方式,我们在"selecPares()" 函数中定义要优化的货币对。EURUSD对应 0 XAUUSD对应 9 。然后声明一个全局bool类型的变量"paramCorrect"。

bool paramCorrect= (selecDePar<9 && maxSpread<50) ||
                   (selecDePar==9 && maxSpread>200);

仅当paramCorrect为true时在OnInit()中执行。

int OnInit()
{   
   ENUM_INIT_RETCODE resp= paramCorrect? INIT_SUCCEEDED: INIT_PARAMETERS_INCORRECT;
   if (paramCorrect)
   {
      //...
      nSimbs= cargaPares(cadParesFX, nombreParesFX);     //返回包含EA所运行货币对的nombreParesFX数组
      //... EA初始化函数
   }
   return(resp);
}

如果paramCorrect为false,那么EA在OnInit()函数中不做任何操作,返回INIT_PARAMETERS_INCORRECT给测策略测试器,说明输入参数组合不正确。当策略测试器从OnInit()中接收到INIT_PARAMETERS_INCORRECT值时,这个参数组合将不会被传递给测试引擎运行,优化测试结果中这一行将为0并且红色高亮显示(见下图)。

使用不正确参数的结果

程序关闭的原因作为输入参数被传递到OnDeinit()中,这有助于了解EA终止运行的原因。然而这是另一回事了。

void OnDeinit(const int motivo)
{
   if(paramCorrect)
   {
      
      //程序关闭函数
      
   }
   infoDeInit(motivo);
   return;
}

//+-------------------------------------- INFORMATION ABOUT THE PROGRAM SHUTDOWN----------------------------
string infoDeInit(int codDeInit)
{                       //告知程序关闭的原因
   string texto= "program initialization...", text1= "CIERRE por: ";
   switch(codDeInit)
   {
      case REASON_PROGRAM:     texto= text1+"The EA finished its work with the ExpertRemove() function"; break;  //0
      case REASON_ACCOUNT:     texto= text1+"The account was changed"; break;                                    //6
      case REASON_CHARTCHANGE: texto= text1+"Symbol or timeframe change"; break;                                 //3
      case REASON_CHARTCLOSE:  texto= text1+"The chart was closed"; break;                                       //4
      case REASON_PARAMETERS:  texto= text1+"Input parameters changed by the user"; break;                       //5
      case REASON_RECOMPILE:   texto= text1+"The program was recompiled"; break //2
      case REASON_REMOVE:      texto= text1+"The program was deleted from the chart"; break;                     //1
      case REASON_TEMPLATE:    texto= text1+"Another chart template was used"; break;                            //7
      case REASON_CLOSE:       texto= text1+"The terminal was closed"; break;                                    //9
      case REASON_INITFAILED:  texto= text1+"The OnInit() handler returned non-zero value"; break;               //8
      default:                 texto= text1+"Other reason";
   }
   Print(texto);
   return(texto);
}

事实上,如果优化器接收到的参数组合在某些时候将参数"paramCorrect"设置为flase时(如当EURUSD的点差设置为100 points时),那么EA将不会运行并且这一步的优化结果为0,这样将可以大大减少不必要的计算机开销和你在MQL5社区租用的计算代理资源。

当然,上述所有这些都可以在OnTesterInit()、ParameterGetRange()和ParameterSetRange()函数中实现,但是上述方法更为简单。此方法比使用OnTesterInit() 更为可靠。


总结

我们讨论了如何在МetaТrader 5中优化时间周期,以及当МetaТrader 5不支持优化字符串变量是如何优化“货币”参数,并且使得其和EA所运行的货币对无关。我们也看到了如何减少优化步骤,去掉不合逻辑的参数组合,保持你的计算机性能并且节省开支。

上述想法并不新鲜,一个新手或者有些许编程经验的程序员都可以实现。这些想法是长期以来对相关信息的收集以及从代码调试的经验中得出的。虽然简单但很实用。你会问我如果MQL5社区要获利,为何要还分享这些想法?答案是战胜程序员的“孤独感”。

谢谢你关注本文。如果你读完了本文并且你是一个经验丰富的程序员,请不吝指正。