Ottimizzazione. Alcune semplici idee

Jose Miguel Soriano | 12 gennaio, 2022

Introduzione

Avendo trovato una strategia coerente per il commercio di EA, la lanciamo sul grafico EURUSD, giusto? La strategia può essere più redditizia su altre coppie di valute? Ci sono altre coppie di valute su cui la strategia darà risultati migliori senza la necessità di aumentare il lotto in progressione geometrica?

Cosa succederà se scegliamo EURUSD e il timeframe H1 standard e poi, se non siamo soddisfatti del risultato, lo cambiamo per EURJPY su H4?

Inoltre, se abbiamo un sistema operativo a 64 bit, che ci consente di smettere di preoccuparci della velocità di test del sistema, ci possiamo dimenticare delle ridicole combinazioni dei parametri di immissione del sistema di trading, che prendono parte all'enumerazione completa durante l'ottimizzazione? Quali risultati dobbiamo trascurare nei rapporti finali?

Ho risolto questi "problemi minori" da solo e in questo articolo condivido le soluzioni efficaci. Apprezzo però che ci possano essere altre soluzioni più ottimali.


Ottimizzazione per tempistiche

MQL5 fornisce un set completo di intervalli di tempo: da M1, M2, M3, M4,... H1, H2,... ai grafici mensili. In totale, ci sono 21 intervalli di tempo. Nel processo di ottimizzazione, tuttavia, vogliamo sapere quali tempi si adattano al meglio alla nostra strategia - quelli brevi come М1 e М5, quelli medi - come H2 e H4 o quelli lunghi - D1 e W1.

Inizialmente non abbiamo bisogno di tutte queste opzioni. In ogni caso, se possiamo vedere che la strategia si dimostra efficace sul periodo di tempo М5, allora nella fase successiva di ottimizzazione possiamo verificare se funzionerà su М3 o М6.

Se usiamo una variabile del tipo ENUM_TIMEFRAMES come parametro di input:

input ENUM_TIMEFRAMES marcoTF= PERIOD_M5; 

allora l'Optimizer offrirà 21 varianti di ottimizzazione. Abbiamo davvero bisogno di questo importo?

Opzioni standard di un intervallo di tempo

Inizialmente no. Come possiamo semplificare l'ottimizzazione? All'inizio possiamo definire l'enumerazione:

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
};

dove possiamo aggiungere o eliminare i tempi di interesse. Per l'ottimizzazione, definire la variabile di input all'inizio del codice:

input mis_MarcosTMP timeframe= _H1;

e definire una nuova funzione nella libreria .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);
}

Dichiarare una nuova variabile nell'ambito delle variabili globali:

ENUM_TIMEFRAMES marcoTmp= defMarcoTiempo(marcoTiempo);          //timeframe is defined as a global variable

"marcoTmp" è una variabile globale, che verrà utilizzata dall'EA per definire un intervallo di tempo del grafico richiesto. Nella tabella dei parametri dell'Ottimizzatore è possibile definire l'intervallo di avvio della variabile "marcoTiempo". Questo coprirà solo i passaggi di nostro interesse senza spendere tempo e risorse per analizzare М6 o М12. In questo modo possiamo analizzare i risultati del lavoro dell'EA su diversi intervalli di tempo.

Opzioni specifiche dell'utente di un intervallo di tempo

Sicuramente, può essere fatto con

ENUM_TIMEFRAMES marcoTmp= (ENUM_TIMEFRAMES)marcoTiempo;

Questo diventa ovvio dopo mesi, anche anni di programmazione se sei un perfezionista e lavori molto sul codice cercando di semplificarlo. Oppure se stai utilizzando un VPS e stai cercando di mantenere basso il conto ottimizzando le prestazioni del computer.


Ottimizzazione di un simbolo o di un insieme di simboli

Nel MetaTrader 5 Strategy Tester c'è una modalità di ottimizzazione, che facilita l'esecuzione dell'EA su tutti i simboli selezionati nella finestra MarketWatch. Questa funzione, tuttavia, non consente di organizzare l'ottimizzazione come se il simbolo selezionato fosse un altro parametro. Quindi, se ci sono 15 simboli selezionati, il Tester organizzerà 15 esecuzioni. Come possiamo scoprire quale simbolo è il migliore per il nostro EA? Se si tratta di un EA multi-valuta, quale gruppo di simboli dà il miglior risultato e con quale set di parametri? Le variabili stringa non vengono ottimizzate in MQL5. Come si può fare?

Codificare un simbolo o una coppia di simboli in base al valore del parametro di input nel modo seguente:

input int selecDePar= 0;

string cadParesFX= selecPares(selecDePar);

Il parametro "selecDePar" viene utilizzato come parametro di ottimizzazione, che viene convertito in una variabile stringa. Usa la variabile "cadParesFX" nell'EA. La coppia di valute / coppie (per questo codice è irrilevante se la strategia è multi-valuta o meno) verranno memorizzate in questa variabile insieme ad altri parametri di ottimizzazione regolari.

//------------------------------------- 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);
}

A seconda di quale sia il nostro obiettivo, definire combinazioni di coppie e informare il Tester dell'intervallo da analizzare. Dare allo Strategy Tester un comando per ottimizzare il parametro "selecDePar" sull'intervallo da 15 a 22 (vedere l'immagine sotto). Cosa facciamo quando vogliamo confrontare i risultati di una moneta unica? In tal caso eseguiamo l'ottimizzazione sull'intervallo da 0 a 9.

Ottimizzazione di un insieme di coppie

Ad esempio, l'EA riceve il valore di cadParesFX= "EURUSD; EURJPY; GBPCHF". In OnInit() chiamare la funzione "cargaPares()", che riempie l'array dinamico arrayPares[] con stringhe, divise per il simbolo ";" nel parametro cadParesFX. Tutte le variabili globali devono essere caricate in matrici dinamiche, che salvano i valori di ogni simbolo, incluso il controllo dell'apertura di una nuova barra su un simbolo, se possibile. Nel caso in cui stiamo lavorando con un simbolo, le dimensioni dell'array saranno uguali a uno.

//-------------------------------- 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);
}

In OnInit() questa funzione è implementata nel modo seguente:

string ar_ParesFX[];    //array, containing names of the pairs for the EA to work with
int numSimbs= 1;        //variable, containing information about the number of symbols it works with

int OnInit()
{
   
   //...
   numSimbs= cargaPares(cadParesFX, ar_ParesFX);     //returns the ar_ParesFX array with pairs for work in the EA
   //...
   
}

Se numSimbs>1, viene chiamata la funzione OnChartEvent(). Questo funziona con un sistema multi-valuta. In caso contrario, viene utilizzata la funzione 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);
      .../...
   }
}
  

Ciò significa che tutte le funzioni nel ruolo del parametro di input devono contenere almeno il simbolo oggetto della richiesta. Ad esempio, invece della funzione Digits(), dobbiamo usare quanto segue:

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

In altre parole, dobbiamo dimenticare le funzioni Symbol() o Point(), così come altre variabili tradizionali per МetaТtarder 4 come Ask o 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);
}

Abbiamo anche dimenticato la funzione di controllo sull'apertura della barra, che è presente è tali codici. Se i tick ricevuti in EURUSD parlano dell'apertura di una nuova barra, i tick USDJPY potrebbero non essere ricevuti nei prossimi 2 secondi. Di conseguenza, sul prossimo tick, USDJPY l'EA sta per scoprire che una nuova barra si sta aprendo per questo simbolo anche se per EURUSD questo evento ha avuto luogo 2 secondi fa.

//------------------------------------- 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);    //received opening time of the current candlestick
        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); 
}

Questo metodo mi ha permesso di scoprire durante un passaggio di ottimizzazione che:

Questo è vero per il periodo di М5 e per una certa combinazione di altri parametri di ottimizzazione, ma non per Н1 o Н2.

C'è un momento imbarazzante qui. Ho chiesto supporto tecnico a riguardo. Non so perché questo stia accadendo, ma il risultato dell'ottimizzazione varia a seconda del simbolo che selezioniamo nello Strategy Tester. Ecco perché per controllare il risultato mantengo questa coppia fissa attraverso lo sviluppo della strategia e mi assicuro che questa sia una di quelle coppie che possono essere analizzate nell'Ottimizzatore.


Ottimizzazione di una combinazione di parametri

A volte, alcune combinazioni illogiche di tutte le combinazioni di parametri che prendono parte all'ottimizzazione risultano essere adatte. Alcuni di loro rendono la strategia irragionevole. Ad esempio, se la variabile della voce "maxSpread" definisce il valore dello spread impostato per un'operazione di trading, ottimizziamo questa variabile per varie coppie in cui lo spread medio del broker è inferiore a 30 e XAUUSD è 400. È assurdo analizzare quelle coppie se superano i 50 e XAUUSD è inferiore a 200. Dopo aver passato i dati all'ottimizzatore, impostare "valuta maxSpread tra 0 e 600 con l'intervallo 20", ma un tale insieme, assieme ad altri parametri produce numerose combinazioni che non hanno senso.

Seguendo il modello descritto nella sezione precedente, abbiamo definito le coppie per l'ottimizzazione nella funzione "selecPares()". A EURUSD viene assegnata l'opzione 0 e a XAUUSD viene assegnata l'opzione 9. Quindi definire una variabile globale del tipo bool "paramCorrect".

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

Eseguire un'azione in OnInit() solo se paramCorrect è nella posizione corretta true.

int OnInit()
{   
   ENUM_INIT_RETCODE resp= paramCorrect? INIT_SUCCEEDED: INIT_PARAMETERS_INCORRECT;
   if (paramCorrect)
   {
      //...
      nSimbs= cargaPares(cadParesFX, nombreParesFX);     //return the array nombreParesFX containing pairs for work in the EA
      //... function of the EA initialization
   }
   return(resp);
}

Se il parametroCorrect si trova nella posizione errata false, l'EA non esegue alcuna azione in OnInit() e restituisce il INIT_PARAMETERS_INCORRECT allo Strategy Tester, il che significa un set di dati di input errato. Quando lo Strategy Tester riceve il valore INIT_PARAMETERS_INCORRECT da OnInit(), questo set di parametri non viene passato ad altri agenti di test per l'implementazione e la riga nella tabella con i risultati dell'ottimizzazione viene riempita con zeri ed evidenziata in rosso (vedere l'immagine seguente).

Risultati dell'utilizzo di parametri non corretti

Il motivo dell'arresto del programma viene passato a OnDeinit() come variabile di input e aiuta a capire il motivo della chiusura di EA. Ma questa è una questione diversa.

void OnDeinit(const int motivo)
{
   if(paramCorrect)
   {
      
      //functions of the program shutdown
      
   }
   infoDeInit(motivo);
   return;
}

//+-------------------------------------- INFORMATION ABOUT THE PROGRAM SHUTDOWN----------------------------
string infoDeInit(int codDeInit)
{                       //informs of the reason of the program shutdown
   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);
}

Il fatto è che se il set di parametri ricevuto dall'Ottimizzatore nella fase specificata imposta "paramCorrect" su false (ad esempio se lo spread EURUSD è stato impostato su 100 punti), allora non eseguiamo l'EA e questo passaggio di ottimizzazione diventa zero senza un uso non necessario delle risorse del computer e delle spese di noleggio degli agenti per il tuo account MQL5.сommunity.

Sicuramente, tutto quanto detto sopra può essere implementato con OnTesterInit() e le funzioni ParameterGetRange() e ParameterSetRange(), ma il modello descritto sembra essere più semplice. Questo è garantito per funzionare mentre il modello con OnTesterInit() non è così coerente.


Conclusione

Abbiamo discusso di velocizzare la ricerca di intervalli di tempo ottimali in МetaТrader 5, ottimizzando il parametro "symbol", quando МetaТrader 5 non consente di ottimizzare le variabili di stringa e rendendolo indifferente al numero di simboli che EA sta utilizzando. Abbiamo anche visto un'illustrazione di come ridurre il numero di passaggi di ottimizzazione eliminando set illogici di parametri di input mantenendo le prestazioni del computer e risparmiando i fondi.

Le idee di cui sopra non sono nuove e possono essere implementate da un principiante o da un programmatore con una certa esperienza. Queste idee erano il risultato di una lunga ricerca di informazioni e dell'utilizzo del programma di debug. Queste sono idee molto semplici ma efficienti. Potresti chiedermi perché li sto condividendo se voglio che MQL5 generi profitto? La risposta è superare la "solitudine" di un programmatore.

Grazie per l'attenzione. Se lo leggi fino alla fine e se sei un programmatore esperto, per favore non giudicarmi troppo duramente.